diff --git a/.gitattributes b/.gitattributes index 00a51aff5..022b84144 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,3 @@ # # These are explicitly windows files and should use crlf *.bat text eol=crlf - diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index bce134066..50c295b5d 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -18,6 +18,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@40166f00814508ec3201fc8595b393d451c8cd80 + - uses: amannn/action-semantic-pull-request@335288255954904a41ddda8947c8f2c844b8bfeb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 338a0985c..37295eb31 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -9,22 +9,23 @@ name: on-merge on: push: - branches: [ master, main ] + branches: + - main permissions: contents: read jobs: build: - + environment: publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 - - name: Set up JDK 8 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f + - name: Set up JDK 17 + uses: actions/setup-java@ebb356cc4e59bcf94f518203228485f5d40e4b58 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven server-id: ossrh @@ -32,12 +33,12 @@ jobs: server-password: ${{ secrets.OSSRH_PASSWORD }} - name: Cache local Maven repository - uses: actions/cache@36f1e144e1c8edb0a652766b484448563d8baf46 + uses: actions/cache@640a1c2554105b57832a23eea0b4672fc7a790d5 with: path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-17-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | - ${{ runner.os }}-maven- + ${{ runner.os }}-17-maven- - name: Configure GPG Key run: | @@ -49,7 +50,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.3.1 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -60,7 +61,7 @@ jobs: # Add -SNAPSHOT before deploy - name: Add SNAPSHOT run: mvn versions:set -DnewVersion='${project.version}-SNAPSHOT' - + - name: Deploy run: | mvn --batch-mode \ diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index d7ade6207..89def2ed2 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -7,36 +7,46 @@ permissions: jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest] + build: + - java: 17 + profile: codequality + - java: 11 + profile: java11 + name: with Java ${{ matrix.build.java }} + runs-on: ${{ matrix.os}} steps: - name: Check out the code - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 + uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f - - name: Set up JDK 8 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + - name: Set up JDK 11 + uses: actions/setup-java@ebb356cc4e59bcf94f518203228485f5d40e4b58 with: - java-version: '8' - distribution: 'temurin' - cache: maven + java-version: ${{ matrix.build.java }} + distribution: 'temurin' + cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/init@dcc1a6637b570d406bec5125dce2e2157d914359 with: languages: java - name: Cache local Maven repository - uses: actions/cache@36f1e144e1c8edb0a652766b484448563d8baf46 + uses: actions/cache@640a1c2554105b57832a23eea0b4672fc7a790d5 with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + path: ~/.m2/repository + key: ${{ runner.os }}${{ matrix.build.java }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}${{ matrix.build.java }}-maven- - name: Verify with Maven - run: mvn --batch-mode --update-snapshots --activate-profiles e2e verify + run: mvn --batch-mode --update-snapshots --activate-profiles e2e,${{ matrix.build.profile }} verify - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.3.1 + - if: matrix.build.java == '17' + name: Upload coverage to Codecov + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -45,4 +55,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/analyze@dcc1a6637b570d406bec5125dce2e2157d914359 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7342889db..53141b448 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,28 +12,37 @@ permissions: # added using https://github.com/step-security/secure-workflows jobs: release-please: - permissions: - contents: write # for google-github-actions/release-please-action to create release commit - pull-requests: write # for google-github-actions/release-please-action to create release PR runs-on: ubuntu-latest + permissions: + contents: write # for googleapis/release-please-action to create release commit + pull-requests: write # for googleapis/release-please-action to create release PR + issues: write # for googleapis/release-please-action to create labels # Release-please creates a PR that tracks all changes steps: - - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee + - uses: googleapis/release-please-action@v4 id: release with: - token: ${{secrets.GITHUB_TOKEN}} - default-branch: main - - # These steps are only run if this was a merged release-please PR - - name: checkout - if: ${{ steps.release.outputs.release_created }} - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 - - name: Set up JDK 8 - if: ${{ steps.release.outputs.release_created }} - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} + outputs: + release_created: ${{ fromJSON(steps.release.outputs.paths_released)[0] != null }} # if we have a single release path, do the release + + publish: + environment: publish + runs-on: ubuntu-latest + permissions: + contents: read + needs: release-please + if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }} + + steps: + - name: Checkout Repository + uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f + + - name: Set up JDK 17 + uses: actions/setup-java@ebb356cc4e59bcf94f518203228485f5d40e4b58 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven server-id: ossrh @@ -41,17 +50,15 @@ jobs: server-password: ${{ secrets.OSSRH_PASSWORD }} - name: Configure GPG Key - if: ${{ steps.release.outputs.release_created }} run: | echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - name: Deploy - if: ${{ steps.release.outputs.release_created }} run: | mvn --batch-mode \ - --settings release/m2-settings.xml clean deploy + --settings release/m2-settings.xml -DskipTests clean deploy env: OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} \ No newline at end of file + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 8e6d4ed29..ca875406d 100644 --- a/.github/workflows/static-code-scanning.yaml +++ b/.github/workflows/static-code-scanning.yaml @@ -29,16 +29,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 + uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/init@dcc1a6637b570d406bec5125dce2e2157d914359 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/autobuild@dcc1a6637b570d406bec5125dce2e2157d914359 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/analyze@dcc1a6637b570d406bec5125dce2e2157d914359 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..2f94e6169 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -16,4 +16,4 @@ # under the License. wrapperVersion=3.3.2 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e2d18dc18..8c4a75878 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.14.0"} \ No newline at end of file +{".":"1.15.1"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7000d46..8d2871346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,197 @@ # Changelog +## [1.15.1](https://github.com/open-feature/java-sdk/compare/v1.14.2...v1.15.1) (2025-05-14) + + +### NOTABLE CHANGES + +* Raise required Java version to 11 ([#1393](https://github.com/open-feature/java-sdk/issues/1393)) + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.22.0 ([#1411](https://github.com/open-feature/java-sdk/issues/1411)) ([e251819](https://github.com/open-feature/java-sdk/commit/e25181982af8e5d37be4876b71b337ca86e8454b)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.22.1 ([#1427](https://github.com/open-feature/java-sdk/issues/1427)) ([1c4d2ef](https://github.com/open-feature/java-sdk/commit/1c4d2efafdebb562f099ba1ec3a6a29eabc8ff91)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.22.2 ([#1442](https://github.com/open-feature/java-sdk/issues/1442)) ([e568f3a](https://github.com/open-feature/java-sdk/commit/e568f3a4f560187586d5473aa7bc12a673340e24)) +* **deps:** update dependency org.projectlombok:lombok to v1.18.38 ([#1403](https://github.com/open-feature/java-sdk/issues/1403)) ([ef32f11](https://github.com/open-feature/java-sdk/commit/ef32f11571de4d3a981efec4f61113eb8b0d7d9d)) +* **deps:** update junit5 monorepo ([#1418](https://github.com/open-feature/java-sdk/issues/1418)) ([97b442e](https://github.com/open-feature/java-sdk/commit/97b442ed6e8f2b99ca949ffd63e5cbf57718c796)) + + +### โœจ New Features + +* add logging on provider state transitions ([#1444](https://github.com/open-feature/java-sdk/issues/1444)) ([e2813b2](https://github.com/open-feature/java-sdk/commit/e2813b2e5df8e548caf16e3e425b35962045ca6c)) +* add telemetry helper utils ([#1346](https://github.com/open-feature/java-sdk/issues/1346)) ([d0ae548](https://github.com/open-feature/java-sdk/commit/d0ae5482771f4d1701bce25381cdf4e92e2d4882)) +* Raise required Java version to 11 ([#1393](https://github.com/open-feature/java-sdk/issues/1393)) ([4dc988b](https://github.com/open-feature/java-sdk/commit/4dc988b637a9e9c377edf7df7b29bf6407319f16)) + + +### ๐Ÿงน Chore + +* add DCO to release please ([45ec4b1](https://github.com/open-feature/java-sdk/commit/45ec4b1b7734c9117f43abf8fe5105c2903c3986)) +* add DCO to release please ([#1429](https://github.com/open-feature/java-sdk/issues/1429)) ([32137bf](https://github.com/open-feature/java-sdk/commit/32137bfa82e9c0391c999bf0be2a36f201620931)) +* add publish env ([#1420](https://github.com/open-feature/java-sdk/issues/1420)) ([665dd51](https://github.com/open-feature/java-sdk/commit/665dd51eb2b3b79d3ffccb6cef64d544aa5e7206)) +* **deps:** update actions/setup-java digest to 148017a ([#1404](https://github.com/open-feature/java-sdk/issues/1404)) ([f834e11](https://github.com/open-feature/java-sdk/commit/f834e11acc7ecf903e972d80e9dab324be97847e)) +* **deps:** update actions/setup-java digest to c5195ef ([#1415](https://github.com/open-feature/java-sdk/issues/1415)) ([a578903](https://github.com/open-feature/java-sdk/commit/a5789038acc36cb2b0ddf12e534a1317e1c9b8e8)) +* **deps:** update actions/setup-java digest to f4f1212 ([#1421](https://github.com/open-feature/java-sdk/issues/1421)) ([a3e2a59](https://github.com/open-feature/java-sdk/commit/a3e2a59aebee051ae8c7eb1c5769a04dc9da8de3)) +* **deps:** update amannn/action-semantic-pull-request digest to 3352882 ([#1434](https://github.com/open-feature/java-sdk/issues/1434)) ([62ba6db](https://github.com/open-feature/java-sdk/commit/62ba6db457358d759fe83f23318b1cf4200756ac)) +* **deps:** update codecov/codecov-action action to v5.4.2 ([#1419](https://github.com/open-feature/java-sdk/issues/1419)) ([a6389e8](https://github.com/open-feature/java-sdk/commit/a6389e89f60aa7f4871f47d78fedd27a7f9991b4)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.4 ([#1414](https://github.com/open-feature/java-sdk/issues/1414)) ([e066d3f](https://github.com/open-feature/java-sdk/commit/e066d3f749c09bb1ef79e3bcace1d205a39787df)) +* **deps:** update dependency com.h3xstream.findsecbugs:findsecbugs-plugin to v1.14.0 ([#1422](https://github.com/open-feature/java-sdk/issues/1422)) ([495da27](https://github.com/open-feature/java-sdk/commit/495da271bee976a942973cd23012f60db895bf24)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10 ([#103](https://github.com/open-feature/java-sdk/issues/103)) ([3403510](https://github.com/open-feature/java-sdk/commit/34035105154b7945c02de2a88fe83eb2414526ef)) +* **deps:** update dependency com.tngtech.archunit:archunit-junit5 to v1.4.1 ([#1440](https://github.com/open-feature/java-sdk/issues/1440)) ([78657ee](https://github.com/open-feature/java-sdk/commit/78657ee79efdc94018387cdf8263a73d4abf7191)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.5 ([#1400](https://github.com/open-feature/java-sdk/issues/1400)) ([1f2d071](https://github.com/open-feature/java-sdk/commit/1f2d0715087ebd4554826d8552b250e4b8b950c8)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.5 ([#1401](https://github.com/open-feature/java-sdk/issues/1401)) ([07301bd](https://github.com/open-feature/java-sdk/commit/07301bda3f5b65550eff1e025fc9c0bec3c25275)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.3 ([#1398](https://github.com/open-feature/java-sdk/issues/1398)) ([1fcf0e7](https://github.com/open-feature/java-sdk/commit/1fcf0e77d956c88c54e10942d96d2afd4d79315c)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.3 ([#1399](https://github.com/open-feature/java-sdk/issues/1399)) ([d6ebc16](https://github.com/open-feature/java-sdk/commit/d6ebc161a93ad703e25592abdb0bf0fd9e281bbc)) +* **deps:** update dependency org.jacoco:jacoco-maven-plugin to v0.8.13 ([#1407](https://github.com/open-feature/java-sdk/issues/1407)) ([e19ccaa](https://github.com/open-feature/java-sdk/commit/e19ccaa35d9ac4d89d72ea58a70d416d202078db)) +* **deps:** update dependency org.mockito:mockito-core to v5.17.0 ([#1409](https://github.com/open-feature/java-sdk/issues/1409)) ([345cdcf](https://github.com/open-feature/java-sdk/commit/345cdcfa10da64c61d769746f335f38ac564e9ad)) +* **deps:** update github/codeql-action digest to 15bce5b ([#1443](https://github.com/open-feature/java-sdk/issues/1443)) ([bc10bac](https://github.com/open-feature/java-sdk/commit/bc10bacb5a68d0d2e498cb41c087505490f19de8)) +* **deps:** update github/codeql-action digest to 2a8cbad ([#1423](https://github.com/open-feature/java-sdk/issues/1423)) ([6b6849f](https://github.com/open-feature/java-sdk/commit/6b6849f3a3ee8a7b66d859c8e522bc101d1ccd44)) +* **deps:** update github/codeql-action digest to 362ef4c ([#1408](https://github.com/open-feature/java-sdk/issues/1408)) ([ca160ca](https://github.com/open-feature/java-sdk/commit/ca160cab7ccd71527e06a0851502353ac50b8d0d)) +* **deps:** update github/codeql-action digest to 40e16ed ([#1437](https://github.com/open-feature/java-sdk/issues/1437)) ([f965cbc](https://github.com/open-feature/java-sdk/commit/f965cbcb37d20724e15b76c15842a88574810b1a)) +* **deps:** update github/codeql-action digest to 4c3e536 ([#1417](https://github.com/open-feature/java-sdk/issues/1417)) ([0c77c84](https://github.com/open-feature/java-sdk/commit/0c77c8446032eaac7e068d48901e1423c21db326)) +* **deps:** update github/codeql-action digest to 4ffa236 ([#1425](https://github.com/open-feature/java-sdk/issues/1425)) ([a7828e7](https://github.com/open-feature/java-sdk/commit/a7828e73a8f2e30f71bd2d9d4da180b2fa436424)) +* **deps:** update github/codeql-action digest to 56dd02f ([#1416](https://github.com/open-feature/java-sdk/issues/1416)) ([4607c62](https://github.com/open-feature/java-sdk/commit/4607c62f15f7ee572207b8ec012ad4b3626e0184)) +* **deps:** update github/codeql-action digest to 5eb3ed6 ([#1439](https://github.com/open-feature/java-sdk/issues/1439)) ([f2348ea](https://github.com/open-feature/java-sdk/commit/f2348ea370412351389c60eef390f36edbea68b0)) +* **deps:** update github/codeql-action digest to 83605b3 ([#1435](https://github.com/open-feature/java-sdk/issues/1435)) ([7e74f2a](https://github.com/open-feature/java-sdk/commit/7e74f2aa3ad2dc8f7a3e4ad398e7705b3e3db364)) +* **deps:** update github/codeql-action digest to 97a2bfd ([#1438](https://github.com/open-feature/java-sdk/issues/1438)) ([85b200a](https://github.com/open-feature/java-sdk/commit/85b200a08b9f8a71de3b5a19eaa057ec04e0801e)) +* **deps:** update github/codeql-action digest to 9f45e74 ([#1396](https://github.com/open-feature/java-sdk/issues/1396)) ([37d76be](https://github.com/open-feature/java-sdk/commit/37d76be697e83f524250a82b2a67cdb4a953d7bc)) +* **deps:** update github/codeql-action digest to d26c46a ([#1413](https://github.com/open-feature/java-sdk/issues/1413)) ([5b327ee](https://github.com/open-feature/java-sdk/commit/5b327eeb770d0a4222f3599be79543b7bed9abc2)) +* **deps:** update github/codeql-action digest to dab8a02 ([#1405](https://github.com/open-feature/java-sdk/issues/1405)) ([5b2f151](https://github.com/open-feature/java-sdk/commit/5b2f1513ab75ef6692978830e59eba87ffa494d5)) +* **deps:** update github/codeql-action digest to e13fe0d ([#1406](https://github.com/open-feature/java-sdk/issues/1406)) ([e211397](https://github.com/open-feature/java-sdk/commit/e211397d517e1263e1251f9c99093bf05cecd93f)) +* **deps:** update github/codeql-action digest to ed51cb5 ([#1436](https://github.com/open-feature/java-sdk/issues/1436)) ([b09e887](https://github.com/open-feature/java-sdk/commit/b09e88798fed529161c61b96c20a8f257d355d3c)) +* **deps:** update github/codeql-action digest to efffb48 ([#1402](https://github.com/open-feature/java-sdk/issues/1402)) ([384953d](https://github.com/open-feature/java-sdk/commit/384953d30ecff83d60a2e5b9790e8228d1a52ac7)) +* **deps:** update github/codeql-action digest to f843d94 ([#1432](https://github.com/open-feature/java-sdk/issues/1432)) ([99faaf8](https://github.com/open-feature/java-sdk/commit/99faaf88aa07bd45fc473db5bafce3b8eafaf9e0)) +* **deps:** update io.cucumber.version to v7.22.0 ([#1410](https://github.com/open-feature/java-sdk/issues/1410)) ([3c69f2f](https://github.com/open-feature/java-sdk/commit/3c69f2f36c4e975d690ecc2e790df632a33001ba)) +* **deps:** update io.cucumber.version to v7.22.1 ([#1426](https://github.com/open-feature/java-sdk/issues/1426)) ([844374a](https://github.com/open-feature/java-sdk/commit/844374a42b94deffab6856e978766354a6f46576)) +* **deps:** update io.cucumber.version to v7.22.2 ([#1441](https://github.com/open-feature/java-sdk/issues/1441)) ([58454b4](https://github.com/open-feature/java-sdk/commit/58454b4eaabfd3327f7ceaff4bf335a5a839ed41)) +* **main:** release 1.15.0 ([#1431](https://github.com/open-feature/java-sdk/issues/1431)) ([7182a7f](https://github.com/open-feature/java-sdk/commit/7182a7fc4197e70218e829971dae2cff09f948c9)) +* update boostrap sha for release please ([f6bd30d](https://github.com/open-feature/java-sdk/commit/f6bd30db93e37e596d211d899315a62d9f810199)) +* update codeowners to give global maintainers code ownership ([#1412](https://github.com/open-feature/java-sdk/issues/1412)) ([498fd38](https://github.com/open-feature/java-sdk/commit/498fd382659669315b0db61db5f19ce054467bc9)) +* update release please action ([#1430](https://github.com/open-feature/java-sdk/issues/1430)) ([1cc851b](https://github.com/open-feature/java-sdk/commit/1cc851b293008a8dd273e904e4c77a650ad71146)) +* use PAT for release please ([014f8a5](https://github.com/open-feature/java-sdk/commit/014f8a59da8f1e976e440ed1ea17e85561f98e2d)) + + +### ๐Ÿ“š Documentation + +* add try-catch example for setProviderAndWait usage ([#1433](https://github.com/open-feature/java-sdk/issues/1433)) ([96cf9c7](https://github.com/open-feature/java-sdk/commit/96cf9c7f5463e4e0de394117845aebdd9a69425f)) + +## [1.14.2](https://github.com/open-feature/java-sdk/compare/v1.14.1...v1.14.2) (2025-03-27) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency org.slf4j:slf4j-api to v2.0.17 ([#1348](https://github.com/open-feature/java-sdk/issues/1348)) ([2ec7c6c](https://github.com/open-feature/java-sdk/commit/2ec7c6c7ff704380fdfd8116378adf78734e4f2b)) +* **deps:** update junit5 monorepo ([#1344](https://github.com/open-feature/java-sdk/issues/1344)) ([d95e270](https://github.com/open-feature/java-sdk/commit/d95e2706532259bd5739e5b4ea4813ef9f2196a6)) +* **deps:** update junit5 monorepo ([#1373](https://github.com/open-feature/java-sdk/issues/1373)) ([6b65e26](https://github.com/open-feature/java-sdk/commit/6b65e26c7439895652c3f64f2b4a7307a7ca582e)) +* equals and hashcode of several classes ([69b571e](https://github.com/open-feature/java-sdk/commit/69b571eda73b6f43c99864420b8663ae54ebf0ad)) +* equals and hashcode of several classes ([#1364](https://github.com/open-feature/java-sdk/issues/1364)) ([69b571e](https://github.com/open-feature/java-sdk/commit/69b571eda73b6f43c99864420b8663ae54ebf0ad)) +* hooks not run in NOT_READY/FATAL ([#1392](https://github.com/open-feature/java-sdk/issues/1392)) ([24ef9dd](https://github.com/open-feature/java-sdk/commit/24ef9dd2903d01ec029b70cd1e39e71ffe327499)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 5a3ec84 ([#1380](https://github.com/open-feature/java-sdk/issues/1380)) ([8359ef1](https://github.com/open-feature/java-sdk/commit/8359ef13bb935ac1d144787cfd7181814a0b286c)) +* **deps:** update actions/cache digest to 7921ae2 ([#1337](https://github.com/open-feature/java-sdk/issues/1337)) ([3920c63](https://github.com/open-feature/java-sdk/commit/3920c638a49caddfb07041f812cc6bc0bf3101f9)) +* **deps:** update actions/cache digest to d4323d4 ([#1353](https://github.com/open-feature/java-sdk/issues/1353)) ([5901797](https://github.com/open-feature/java-sdk/commit/59017977a487a36c8a39f63b83299bc657134c0d)) +* **deps:** update actions/setup-java digest to 3b6c050 ([#1391](https://github.com/open-feature/java-sdk/issues/1391)) ([7536679](https://github.com/open-feature/java-sdk/commit/753667925a8803b3b227f762936ae397dde95484)) +* **deps:** update actions/setup-java digest to 799ee7c ([#1359](https://github.com/open-feature/java-sdk/issues/1359)) ([31444d6](https://github.com/open-feature/java-sdk/commit/31444d6c8f30f0dd35debacc9dab8da7397e11ed)) +* **deps:** update actions/setup-java digest to b8ebb8b ([#1381](https://github.com/open-feature/java-sdk/issues/1381)) ([2239f05](https://github.com/open-feature/java-sdk/commit/2239f054b90734dde6cdd4a23daec1c1daa96f07)) +* **deps:** update amannn/action-semantic-pull-request digest to 04501d4 ([#1390](https://github.com/open-feature/java-sdk/issues/1390)) ([87c06d9](https://github.com/open-feature/java-sdk/commit/87c06d9edd935287daf7ebc8db1e7da4831531de)) +* **deps:** update codecov/codecov-action action to v5.4.0 ([#1351](https://github.com/open-feature/java-sdk/issues/1351)) ([b133c2f](https://github.com/open-feature/java-sdk/commit/b133c2fa527a0dddb6de7f7781a00fc84feaa813)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.3 ([#1341](https://github.com/open-feature/java-sdk/issues/1341)) ([5de33c0](https://github.com/open-feature/java-sdk/commit/5de33c02a675db6ca5966bfa3f58d99c8e53e36b)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.1.0 ([#1332](https://github.com/open-feature/java-sdk/issues/1332)) ([cdcdc14](https://github.com/open-feature/java-sdk/commit/cdcdc143ea5ad2f003cb3f5450ec78314e619ea3)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.2.0 ([#1360](https://github.com/open-feature/java-sdk/issues/1360)) ([ecea9df](https://github.com/open-feature/java-sdk/commit/ecea9df932ee4874613f219b73640fe964c99593)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.3.0 ([#1375](https://github.com/open-feature/java-sdk/issues/1375)) ([de3e213](https://github.com/open-feature/java-sdk/commit/de3e213ac8b8931121904a3d12929405512e74dd)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.2 ([#1355](https://github.com/open-feature/java-sdk/issues/1355)) ([2a1adca](https://github.com/open-feature/java-sdk/commit/2a1adca8c2ed8d61d51530969290793a5d3d15f3)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.3 ([#1384](https://github.com/open-feature/java-sdk/issues/1384)) ([b6becac](https://github.com/open-feature/java-sdk/commit/b6becac2c4e0f98a8651cc2f77d4c0b081548991)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.4 ([#1387](https://github.com/open-feature/java-sdk/issues/1387)) ([cb574d9](https://github.com/open-feature/java-sdk/commit/cb574d93b6210c89a188aa104ef4f1db68daf1c0)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.2 ([#1356](https://github.com/open-feature/java-sdk/issues/1356)) ([dd83114](https://github.com/open-feature/java-sdk/commit/dd83114c4d9389753575392fafcd56585d7178ae)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.3 ([#1385](https://github.com/open-feature/java-sdk/issues/1385)) ([4125ae8](https://github.com/open-feature/java-sdk/commit/4125ae83801a9f485059a9edaca090ee47b7632f)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.4 ([#1388](https://github.com/open-feature/java-sdk/issues/1388)) ([d8f6514](https://github.com/open-feature/java-sdk/commit/d8f6514598d53f43cb084ee746742a59d271363b)) +* **deps:** update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.0 ([#1342](https://github.com/open-feature/java-sdk/issues/1342)) ([88a778c](https://github.com/open-feature/java-sdk/commit/88a778cc03e112d45756428d1f0ae1ef0fe02c84)) +* **deps:** update dependency org.awaitility:awaitility to v4.3.0 ([#1343](https://github.com/open-feature/java-sdk/issues/1343)) ([1504d0f](https://github.com/open-feature/java-sdk/commit/1504d0f7982757a2b413eda593ce7057b90519e5)) +* **deps:** update dependency org.mockito:mockito-core to v5.15.2 ([#1339](https://github.com/open-feature/java-sdk/issues/1339)) ([4817864](https://github.com/open-feature/java-sdk/commit/4817864fd7ae70c1e19c3c09e82e1fb03dd88942)) +* **deps:** update dependency org.mockito:mockito-core to v5.16.0 ([#1358](https://github.com/open-feature/java-sdk/issues/1358)) ([30b6d00](https://github.com/open-feature/java-sdk/commit/30b6d004aaf3464547805f7eda6fad0e122de4f9)) +* **deps:** update dependency org.mockito:mockito-core to v5.16.1 ([#1376](https://github.com/open-feature/java-sdk/issues/1376)) ([9750f75](https://github.com/open-feature/java-sdk/commit/9750f75d04beb8339fc2e972f0ee97120eaff354)) +* **deps:** update github/codeql-action digest to 1bb15d0 ([#1336](https://github.com/open-feature/java-sdk/issues/1336)) ([e163ce1](https://github.com/open-feature/java-sdk/commit/e163ce1c060d0dc8812e4a8a3b37f52b0156324d)) +* **deps:** update github/codeql-action digest to 486ab5a ([#1389](https://github.com/open-feature/java-sdk/issues/1389)) ([85fd5e0](https://github.com/open-feature/java-sdk/commit/85fd5e0997ff1a5e5d7226d8bbfe2775769a6ca6)) +* **deps:** update github/codeql-action digest to 56b25d5 ([#1365](https://github.com/open-feature/java-sdk/issues/1365)) ([959e675](https://github.com/open-feature/java-sdk/commit/959e675e4c2363e5fd80d1d2f1edbfab11794fc8)) +* **deps:** update github/codeql-action digest to 608ccd6 ([#1361](https://github.com/open-feature/java-sdk/issues/1361)) ([67b34f8](https://github.com/open-feature/java-sdk/commit/67b34f84a373512013ab2f7649faaddfd2d61048)) +* **deps:** update github/codeql-action digest to 6349095 ([#1378](https://github.com/open-feature/java-sdk/issues/1378)) ([dbf92df](https://github.com/open-feature/java-sdk/commit/dbf92df33bf5657d50dc3b2f129207b0097c1f27)) +* **deps:** update github/codeql-action digest to 6a151cd ([#1377](https://github.com/open-feature/java-sdk/issues/1377)) ([7065655](https://github.com/open-feature/java-sdk/commit/706565581d78856dd73605b1a16b131f974c0731)) +* **deps:** update github/codeql-action digest to 70df9de ([#1372](https://github.com/open-feature/java-sdk/issues/1372)) ([d233480](https://github.com/open-feature/java-sdk/commit/d233480912f1d5e095f5034f36a838535d1ecdff)) +* **deps:** update github/codeql-action digest to 7254660 ([#1368](https://github.com/open-feature/java-sdk/issues/1368)) ([d54c68a](https://github.com/open-feature/java-sdk/commit/d54c68a8e9e4a0f67c99e7d76621a1c5724e4cd1)) +* **deps:** update github/codeql-action digest to 80f9930 ([#1357](https://github.com/open-feature/java-sdk/issues/1357)) ([6c03e5d](https://github.com/open-feature/java-sdk/commit/6c03e5d84aacee11f5b8e608a6114c11fced72b8)) +* **deps:** update github/codeql-action digest to 8392354 ([#1352](https://github.com/open-feature/java-sdk/issues/1352)) ([989f4ae](https://github.com/open-feature/java-sdk/commit/989f4ae54263b46ca2c81561acc70b39918c382d)) +* **deps:** update github/codeql-action digest to 8c1551c ([#1333](https://github.com/open-feature/java-sdk/issues/1333)) ([859a36c](https://github.com/open-feature/java-sdk/commit/859a36cbfafc94d4601b87d304237e6ddf97c08d)) +* **deps:** update github/codeql-action digest to 8c69433 ([#1347](https://github.com/open-feature/java-sdk/issues/1347)) ([6987568](https://github.com/open-feature/java-sdk/commit/698756856ba40e98d91ccf661dab409798861aa5)) +* **deps:** update github/codeql-action digest to 97aac9b ([#1350](https://github.com/open-feature/java-sdk/issues/1350)) ([7df9565](https://github.com/open-feature/java-sdk/commit/7df9565691731d164b534116b8a6b933b171d103)) +* **deps:** update github/codeql-action digest to a8849fb ([#1345](https://github.com/open-feature/java-sdk/issues/1345)) ([de64edd](https://github.com/open-feature/java-sdk/commit/de64eddfb3a6cc117bb108dbcf167830e9f6729d)) +* **deps:** update github/codeql-action digest to acadfed ([#1335](https://github.com/open-feature/java-sdk/issues/1335)) ([5436eb0](https://github.com/open-feature/java-sdk/commit/5436eb0d5db3a0e9bd9289fbef57b9eeada0a667)) +* **deps:** update github/codeql-action digest to b2e6519 ([#1366](https://github.com/open-feature/java-sdk/issues/1366)) ([d00e4b5](https://github.com/open-feature/java-sdk/commit/d00e4b5b24621aa55085827fbe6ea982491376de)) +* **deps:** update github/codeql-action digest to b46b37a ([#1367](https://github.com/open-feature/java-sdk/issues/1367)) ([c550d59](https://github.com/open-feature/java-sdk/commit/c550d597227bfc1e0e17357139f1fd8a87593be0)) +* **deps:** update github/codeql-action digest to bd1d9ab ([#1383](https://github.com/open-feature/java-sdk/issues/1383)) ([922e17e](https://github.com/open-feature/java-sdk/commit/922e17e677e15690e3df2fe93a961f16f21ff283)) +* **deps:** update github/codeql-action digest to c50c157 ([#1379](https://github.com/open-feature/java-sdk/issues/1379)) ([d61c33e](https://github.com/open-feature/java-sdk/commit/d61c33e466336c7120b870ca5e3843eba5f7175c)) +* **deps:** update github/codeql-action digest to d99c7e8 ([#1338](https://github.com/open-feature/java-sdk/issues/1338)) ([4e535fd](https://github.com/open-feature/java-sdk/commit/4e535fd10fac742ca472faa62c941fa51b282ca7)) +* **deps:** update github/codeql-action digest to dc49dca ([#1369](https://github.com/open-feature/java-sdk/issues/1369)) ([f8df5fb](https://github.com/open-feature/java-sdk/commit/f8df5fb84a765af917587dd509f9cec38103f787)) +* **deps:** update github/codeql-action digest to e0ea141 ([#1386](https://github.com/open-feature/java-sdk/issues/1386)) ([387e5f2](https://github.com/open-feature/java-sdk/commit/387e5f2e3bd24ccea6691b0d6dbfe542cfd05b52)) +* **deps:** update github/codeql-action digest to ff79de6 ([#1340](https://github.com/open-feature/java-sdk/issues/1340)) ([50b45b2](https://github.com/open-feature/java-sdk/commit/50b45b2be442bb89a431c9bcc45d825f63bd93a6)) +* update build and tooling to utilize new java version ([#1321](https://github.com/open-feature/java-sdk/issues/1321)) ([90217b2](https://github.com/open-feature/java-sdk/commit/90217b2083a2ba92c623365dc450326d49b46fab)) + +## [1.14.1](https://github.com/open-feature/java-sdk/compare/v1.14.0...v1.14.1) (2025-02-14) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.21.0 ([#1312](https://github.com/open-feature/java-sdk/issues/1312)) ([208411e](https://github.com/open-feature/java-sdk/commit/208411e72338e37bf477ac0b784bbbbe0309b922)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.21.1 ([#1317](https://github.com/open-feature/java-sdk/issues/1317)) ([b797883](https://github.com/open-feature/java-sdk/commit/b7978832b786fe081169ff0efeb702218300c622)) +* possible event-related deadlocks with some providers ([#1314](https://github.com/open-feature/java-sdk/issues/1314)) ([c33ac2d](https://github.com/open-feature/java-sdk/commit/c33ac2d9b2e91b85fffb3c21653912fe82006351)) +* TrackingEventDetails interface to include numeric getValue() call ([#1328](https://github.com/open-feature/java-sdk/issues/1328)) ([08c38fb](https://github.com/open-feature/java-sdk/commit/08c38fb553d82a42682c3eb9239329f770063898)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 9fa7e61 ([#1324](https://github.com/open-feature/java-sdk/issues/1324)) ([69cdc77](https://github.com/open-feature/java-sdk/commit/69cdc772a639470dd223bf70ef6e9f8bc4d93dea)) +* **deps:** update actions/checkout digest to 85e6279 ([#1287](https://github.com/open-feature/java-sdk/issues/1287)) ([640e35e](https://github.com/open-feature/java-sdk/commit/640e35e85375e3098f61b7397432d80a95502bdd)) +* **deps:** update actions/setup-java digest to 28b532b ([#1296](https://github.com/open-feature/java-sdk/issues/1296)) ([874e86d](https://github.com/open-feature/java-sdk/commit/874e86df5c22a1e5771ca16c76aa13039b5f9b65)) +* **deps:** update actions/setup-java digest to 3a4f6e1 ([#1306](https://github.com/open-feature/java-sdk/issues/1306)) ([ba9cc4b](https://github.com/open-feature/java-sdk/commit/ba9cc4b85a1082d638d49b9d2d0a4ed5a45f09ee)) +* **deps:** update actions/setup-java digest to 51ab6d2 ([#1288](https://github.com/open-feature/java-sdk/issues/1288)) ([c69d3a4](https://github.com/open-feature/java-sdk/commit/c69d3a4bd137c1d6baa47c14228bfe8f96555676)) +* **deps:** update actions/setup-java digest to 99d3141 ([#1285](https://github.com/open-feature/java-sdk/issues/1285)) ([32a3933](https://github.com/open-feature/java-sdk/commit/32a39335de8e61650905fc96dc1a73e65f1fe9f8)) +* **deps:** update codecov/codecov-action action to v5.2.0 ([#1298](https://github.com/open-feature/java-sdk/issues/1298)) ([531fc38](https://github.com/open-feature/java-sdk/commit/531fc385b662c5b7b334fee298fc9fe1283c78fb)) +* **deps:** update codecov/codecov-action action to v5.3.0 ([#1301](https://github.com/open-feature/java-sdk/issues/1301)) ([f7f6586](https://github.com/open-feature/java-sdk/commit/f7f6586d72e3f112a7dafc8f77de273ed49ccc4b)) +* **deps:** update codecov/codecov-action action to v5.3.1 ([#1303](https://github.com/open-feature/java-sdk/issues/1303)) ([f9fa54b](https://github.com/open-feature/java-sdk/commit/f9fa54be493e1d0843b709008eb0f047e7580d47)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.16.0 ([#1289](https://github.com/open-feature/java-sdk/issues/1289)) ([0b5b423](https://github.com/open-feature/java-sdk/commit/0b5b423bdd378bb1db3e10fe5da7fa2c937a4610)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.16.1 ([#1292](https://github.com/open-feature/java-sdk/issues/1292)) ([0af9f29](https://github.com/open-feature/java-sdk/commit/0af9f2901f88b5ef9bed0c570d426939a55af3cf)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.0 ([#1309](https://github.com/open-feature/java-sdk/issues/1309)) ([cda3405](https://github.com/open-feature/java-sdk/commit/cda34053f7e39318205a181ef93c825bab2ed9fc)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.1 ([#1329](https://github.com/open-feature/java-sdk/issues/1329)) ([9ab2618](https://github.com/open-feature/java-sdk/commit/9ab26182eae4974b60d166777c51dfcb07957150)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.16.0 ([#1290](https://github.com/open-feature/java-sdk/issues/1290)) ([6c4205a](https://github.com/open-feature/java-sdk/commit/6c4205a00817af260ef9b90f54ce878cad33f75a)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.16.1 ([#1293](https://github.com/open-feature/java-sdk/issues/1293)) ([6071932](https://github.com/open-feature/java-sdk/commit/6071932cb4207dc83cdedfa67c8a69ed71d9c26a)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.0 ([#1310](https://github.com/open-feature/java-sdk/issues/1310)) ([40fa173](https://github.com/open-feature/java-sdk/commit/40fa1733382f4c476a1228c6499044ad83c8f3c4)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.1 ([#1330](https://github.com/open-feature/java-sdk/issues/1330)) ([4ba5695](https://github.com/open-feature/java-sdk/commit/4ba5695eeea6a7ab2fe1d2c595fa482d4b7868dc)) +* **deps:** update dependency org.assertj:assertj-core to v3.27.3 ([#1291](https://github.com/open-feature/java-sdk/issues/1291)) ([a5eb21d](https://github.com/open-feature/java-sdk/commit/a5eb21d1a2e6945a4455cacde898bc913bddb96d)) +* **deps:** update github/codeql-action digest to 0701025 ([#1311](https://github.com/open-feature/java-sdk/issues/1311)) ([9a1e9ab](https://github.com/open-feature/java-sdk/commit/9a1e9abd64220c8d8706f2a64e041ef3f37e1a43)) +* **deps:** update github/codeql-action digest to 08bc0cf ([#1313](https://github.com/open-feature/java-sdk/issues/1313)) ([37ed6a4](https://github.com/open-feature/java-sdk/commit/37ed6a424cdc013ed74c9881826cc56c93ae8228)) +* **deps:** update github/codeql-action digest to 0a35e8f ([#1316](https://github.com/open-feature/java-sdk/issues/1316)) ([26e1d7f](https://github.com/open-feature/java-sdk/commit/26e1d7fff342a32880542efa87b017aec506667e)) +* **deps:** update github/codeql-action digest to 0f1559a ([#1286](https://github.com/open-feature/java-sdk/issues/1286)) ([882d2dd](https://github.com/open-feature/java-sdk/commit/882d2dd5bdac007e8a3783efc54fa45faed22054)) +* **deps:** update github/codeql-action digest to 10a3f07 ([#1280](https://github.com/open-feature/java-sdk/issues/1280)) ([a3854d6](https://github.com/open-feature/java-sdk/commit/a3854d6ab1dba99f4db18f868e89fcc04418e306)) +* **deps:** update github/codeql-action digest to 1c15a48 ([#1325](https://github.com/open-feature/java-sdk/issues/1325)) ([3baf0df](https://github.com/open-feature/java-sdk/commit/3baf0df966f8212864aa7e57bc3d3d09d324fe11)) +* **deps:** update github/codeql-action digest to 1efc6bb ([#1281](https://github.com/open-feature/java-sdk/issues/1281)) ([8a1ab7e](https://github.com/open-feature/java-sdk/commit/8a1ab7ea18aff4ee5a6a2fdd1f805b08e51a50a3)) +* **deps:** update github/codeql-action digest to 24e1c2d ([#1315](https://github.com/open-feature/java-sdk/issues/1315)) ([46903c6](https://github.com/open-feature/java-sdk/commit/46903c6f275e5f9dc8884acf3f76f76efcfc58bd)) +* **deps:** update github/codeql-action digest to 3b4f4d9 ([#1282](https://github.com/open-feature/java-sdk/issues/1282)) ([b390d5f](https://github.com/open-feature/java-sdk/commit/b390d5f0b0945948cd6b87e6486725d095d5ac8a)) +* **deps:** update github/codeql-action digest to 43cffee ([#1304](https://github.com/open-feature/java-sdk/issues/1304)) ([6874de6](https://github.com/open-feature/java-sdk/commit/6874de64ce589e853f5523019bfa9e1d60840baf)) +* **deps:** update github/codeql-action digest to 54b1c84 ([#1307](https://github.com/open-feature/java-sdk/issues/1307)) ([6f36434](https://github.com/open-feature/java-sdk/commit/6f36434c520dcef27deb04e04941693dc15acb2f)) +* **deps:** update github/codeql-action digest to 5f4f998 ([#1305](https://github.com/open-feature/java-sdk/issues/1305)) ([7916d76](https://github.com/open-feature/java-sdk/commit/7916d76635c5ab59dafe6d72058aad9cfcf05f4b)) +* **deps:** update github/codeql-action digest to 6063925 ([#1320](https://github.com/open-feature/java-sdk/issues/1320)) ([538140d](https://github.com/open-feature/java-sdk/commit/538140dfe713a421623b179e69b399f82200fe61)) +* **deps:** update github/codeql-action digest to 7e3036b ([#1300](https://github.com/open-feature/java-sdk/issues/1300)) ([3491956](https://github.com/open-feature/java-sdk/commit/34919561b73faa0cca489ad480e93cca9a854167)) +* **deps:** update github/codeql-action digest to 87fc816 ([#1277](https://github.com/open-feature/java-sdk/issues/1277)) ([c2a82db](https://github.com/open-feature/java-sdk/commit/c2a82dbdbafa134fae4b0c9aef88cf589e09aefa)) +* **deps:** update github/codeql-action digest to 93da9f2 ([#1283](https://github.com/open-feature/java-sdk/issues/1283)) ([45b3995](https://github.com/open-feature/java-sdk/commit/45b3995bdad9f1b05abb01455a9c8f57028cfde5)) +* **deps:** update github/codeql-action digest to affec20 ([#1323](https://github.com/open-feature/java-sdk/issues/1323)) ([8f3ced5](https://github.com/open-feature/java-sdk/commit/8f3ced590764760244cc81ac10c939ca62504dfe)) +* **deps:** update github/codeql-action digest to b44b19f ([#1297](https://github.com/open-feature/java-sdk/issues/1297)) ([305e032](https://github.com/open-feature/java-sdk/commit/305e0329e78116fe697240e420879ac85012d698)) +* **deps:** update github/codeql-action digest to d90e07f ([#1294](https://github.com/open-feature/java-sdk/issues/1294)) ([5671184](https://github.com/open-feature/java-sdk/commit/5671184e7f76f979d631c18bb2ebfb15dccfb207)) +* **deps:** update github/codeql-action digest to db7177a ([#1279](https://github.com/open-feature/java-sdk/issues/1279)) ([b997946](https://github.com/open-feature/java-sdk/commit/b997946db1c7663b7ebb775ad45cdb2b0aaeb291)) +* **deps:** update github/codeql-action digest to e7c0c9d ([#1302](https://github.com/open-feature/java-sdk/issues/1302)) ([78adc77](https://github.com/open-feature/java-sdk/commit/78adc77c23da6116e1f58b3a45dc283c3c58837b)) +* **deps:** update github/codeql-action digest to e9987ad ([#1308](https://github.com/open-feature/java-sdk/issues/1308)) ([99d8185](https://github.com/open-feature/java-sdk/commit/99d818572a3407ca6b25f6e91f69ef3e83bdc657)) +* **deps:** update github/codeql-action digest to f89b8a7 ([#1295](https://github.com/open-feature/java-sdk/issues/1295)) ([122e82f](https://github.com/open-feature/java-sdk/commit/122e82f8431fb116ae3b147f7e2245d7f90b1c77)) + ## [1.14.0](https://github.com/open-feature/java-sdk/compare/v1.13.0...v1.14.0) (2025-01-10) diff --git a/CODEOWNERS b/CODEOWNERS index e75c1d5f1..342eb8df1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,4 +3,4 @@ # # Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-java/workgroup.yaml # -* @open-feature/sdk-java-maintainers +* @open-feature/sdk-java-maintainers @open-feature/maintainers diff --git a/README.md b/README.md index cbb9d9f13..6593c9b1e 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - Release + + Release @@ -46,7 +46,7 @@ ### Requirements -- Java 8+ (compiler target is 1.8) +- Java 11+ (compiler target is 11) Note that this library is intended to be used in server-side contexts and has not been evaluated for use on mobile devices. @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.14.0 + 1.15.1 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.14.0' + implementation 'dev.openfeature:sdk:1.15.1' } ``` @@ -104,7 +104,12 @@ public void example(){ // configure a provider OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new InMemoryProvider(myFlags)); + try { + api.setProviderAndWait(new InMemoryProvider(myFlags)); + } catch (Exception e) { + // handle initialization failure + e.printStackTrace(); + } // create a client Client client = api.getClient(); @@ -149,7 +154,12 @@ To register a provider in a blocking manner to ensure it is ready before further ```java OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new MyProvider()); + try { + api.setProviderAndWait(new MyProvider()); + } catch (Exception e) { + // handle initialization failure + e.printStackTrace(); + } ``` #### Asynchronous diff --git a/pom.xml b/pom.xml index e75dd506a..433487606 100644 --- a/pom.xml +++ b/pom.xml @@ -5,16 +5,20 @@ dev.openfeature sdk - 1.14.0 + 1.15.1 + [17,) UTF-8 - 1.8 + 11 ${maven.compiler.source} - 5.11.4 + 5.18.0 **/e2e/*.java ${project.groupId}.${project.artifactId} + false + + 11 OpenFeature Java SDK @@ -48,7 +52,7 @@ org.projectlombok lombok - 1.18.36 + 1.18.38 provided @@ -63,14 +67,21 @@ org.slf4j slf4j-api - 2.0.16 + 2.0.17 + + com.tngtech.archunit + archunit-junit5 + 1.4.1 + test + + org.mockito mockito-core - 4.11.0 + ${org.mockito.version} test @@ -84,35 +95,30 @@ org.junit.jupiter junit-jupiter - ${junit.jupiter.version} test org.junit.jupiter junit-jupiter-engine - ${junit.jupiter.version} test org.junit.jupiter junit-jupiter-api - ${junit.jupiter.version} test org.junit.jupiter junit-jupiter-params - ${junit.jupiter.version} test org.junit.platform junit-platform-suite - 1.11.4 test @@ -128,6 +134,12 @@ test + + io.cucumber + cucumber-picocontainer + test + + org.simplify4u slf4j2-mock @@ -145,7 +157,7 @@ org.awaitility awaitility - 4.2.2 + 4.3.0 test @@ -167,14 +179,14 @@ net.bytebuddy byte-buddy - 1.17.0 + 1.17.6 test net.bytebuddy byte-buddy-agent - 1.17.0 + 1.17.6 test @@ -182,7 +194,7 @@ io.cucumber cucumber-bom - 7.21.0 + 7.23.0 pom import @@ -190,7 +202,7 @@ org.junit junit-bom - 5.11.4 + 5.13.2 pom import @@ -200,6 +212,18 @@ + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + select-jdk-toolchain + + + + org.cyclonedx cyclonedx-maven-plugin @@ -226,47 +250,22 @@ - - maven-dependency-plugin - 3.8.1 - - - verify - - analyze - - - - - true - - com.github.spotbugs:* - org.junit* - org.simplify4u:slf4j2-mock* - - - com.google.guava* - io.cucumber* - org.junit* - com.google.code.findbugs* - com.github.spotbugs* - org.simplify4u:slf4j-mock-common:* - - - - maven-compiler-plugin - 3.13.0 + 3.14.0 org.apache.maven.plugins maven-surefire-plugin - 3.5.2 + 3.5.3 + 1 + false ${surefireArgLine} + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED @@ -278,7 +277,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.5.2 + 3.5.3 ${surefireArgLine} @@ -286,65 +285,6 @@ - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - - - prepare-agent - - prepare-agent - - - - ${project.build.directory}/coverage-reports/jacoco-ut.exec - surefireArgLine - - - - - report - verify - - report - - - - ${project.build.directory}/coverage-reports/jacoco-ut.exec - ${project.reporting.outputDirectory}/jacoco-ut - - - - - jacoco-check - - check - - - ${project.build.directory}/coverage-reports/jacoco-ut.exec - - dev/openfeature/sdk/exceptions/** - - - - - PACKAGE - - - LINE - COVEREDRATIO - 0.80 - - - - - - - - - org.apache.maven.plugins @@ -359,134 +299,217 @@ - - org.apache.maven.plugins - maven-pmd-plugin - 3.26.0 - - - run-pmd - verify - - check - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.6.6 - - spotbugs-exclusions.xml - - - com.h3xstream.findsecbugs - findsecbugs-plugin - 1.13.0 - - - - - - - com.github.spotbugs - spotbugs - 4.8.6 - - - - - run-spotbugs - verify - - check - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - checkstyle.xml - UTF-8 - true - true - false - - - - com.puppycrawl.tools - checkstyle - 9.3 - - - - - validate - validate - - check - - - - - - com.diffplug.spotless - spotless-maven-plugin - 2.30.0 - - - - - - - - - .gitattributes - .gitignore - - - - - - true - 4 - - - - - - - - - true - 4 - - - - - - - - - - - - check - - - - - + + codequality + + true + + + + + maven-dependency-plugin + 3.8.1 + + + verify + + analyze + + + + + true + + com.github.spotbugs:* + org.junit* + com.tngtech.archunit* + org.simplify4u:slf4j2-mock* + + + com.google.guava* + io.cucumber* + org.junit* + com.tngtech.archunit* + com.google.code.findbugs* + com.github.spotbugs* + org.simplify4u:slf4j-mock-common:* + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.13 + + + + prepare-agent + + prepare-agent + + + + ${project.build.directory}/coverage-reports/jacoco-ut.exec + surefireArgLine + + + + + report + verify + + report + + + + ${project.build.directory}/coverage-reports/jacoco-ut.exec + ${project.reporting.outputDirectory}/jacoco-ut + + + + + jacoco-check + + check + + + ${project.build.directory}/coverage-reports/jacoco-ut.exec + + dev/openfeature/sdk/exceptions/** + + + + + PACKAGE + + + LINE + COVEREDRATIO + 0.80 + + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.9.3.2 + + spotbugs-exclusions.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + 1.14.0 + + + + + + + com.github.spotbugs + spotbugs + 4.8.6 + + + + + run-spotbugs + verify + + check + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + checkstyle.xml + true + true + false + + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + validate + validate + + check + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.5 + + + + + + + + + .gitattributes + .gitignore + + + + + + true + 4 + + + + + + + + + true + 4 + + + + + + + + + + + + check + + + + + + + deploy @@ -589,7 +612,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.0 + 3.5.1 update-test-harness-submodule @@ -608,19 +631,75 @@ + + + + + + + + + java11 + + + + [11,) + true + + + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + select-jdk-toolchain + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + ${surefireArgLine} + + + + ${testExclusions} + + + ${skip.tests} + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + ${surefireArgLine} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + - copy-evaluation-gherkin-tests - validate + default-testCompile + test-compile - exec + testCompile - - cp - - spec/specification/assets/gherkin/evaluation.feature - src/test/resources/features/ - + true diff --git a/release-please-config.json b/release-please-config.json index ad00d89a5..bc4fa6b53 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,5 +1,6 @@ { - "bootstrap-sha": "c701a6c4ebbe1170a25ca7636a31508b9628831c", + "bootstrap-sha": "d7b591c9f910afad303d6d814f65c7f9dab33b89", + "signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>", "packages": { ".": { "package-name": "dev.openfeature.sdk", diff --git a/src/main/java/dev/openfeature/sdk/AbstractStructure.java b/src/main/java/dev/openfeature/sdk/AbstractStructure.java index 6c652114c..7962705c3 100644 --- a/src/main/java/dev/openfeature/sdk/AbstractStructure.java +++ b/src/main/java/dev/openfeature/sdk/AbstractStructure.java @@ -3,15 +3,17 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import lombok.EqualsAndHashCode; @SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) +@EqualsAndHashCode abstract class AbstractStructure implements Structure { protected final Map attributes; @Override public boolean isEmpty() { - return attributes == null || attributes.size() == 0; + return attributes == null || attributes.isEmpty(); } AbstractStructure() { diff --git a/src/main/java/dev/openfeature/sdk/Awaitable.java b/src/main/java/dev/openfeature/sdk/Awaitable.java new file mode 100644 index 000000000..7d5f477dc --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/Awaitable.java @@ -0,0 +1,44 @@ +package dev.openfeature.sdk; + +/** + * A class to help with synchronization by allowing the optional awaiting of the associated action. + */ +public class Awaitable { + + /** + * An already-completed Awaitable. Awaiting this will return immediately. + */ + public static final Awaitable FINISHED = new Awaitable(true); + + private boolean isDone = false; + + public Awaitable() {} + + private Awaitable(boolean isDone) { + this.isDone = isDone; + } + + /** + * Lets the calling thread wait until some other thread calls {@link Awaitable#wakeup()}. If + * {@link Awaitable#wakeup()} has been called before the current thread invokes this method, it will return + * immediately. + */ + @SuppressWarnings("java:S2142") + public synchronized void await() { + while (!isDone) { + try { + this.wait(); + } catch (InterruptedException ignored) { + // ignored, do not propagate the interrupted state + } + } + } + + /** + * Wakes up all threads that have called {@link Awaitable#await()} and lets them proceed. + */ + public synchronized void wakeup() { + isDone = true; + this.notifyAll(); + } +} diff --git a/src/main/java/dev/openfeature/sdk/EvaluationEvent.java b/src/main/java/dev/openfeature/sdk/EvaluationEvent.java new file mode 100644 index 000000000..f92e24d5a --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EvaluationEvent.java @@ -0,0 +1,24 @@ +package dev.openfeature.sdk; + +import java.util.HashMap; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +/** + * Represents an evaluation event. + */ +@Builder +@Getter +public class EvaluationEvent { + + private String name; + + @Singular("attribute") + private Map attributes; + + public Map getAttributes() { + return new HashMap<>(attributes); + } +} diff --git a/src/main/java/dev/openfeature/sdk/EventDetails.java b/src/main/java/dev/openfeature/sdk/EventDetails.java index e32e61013..c75b046e0 100644 --- a/src/main/java/dev/openfeature/sdk/EventDetails.java +++ b/src/main/java/dev/openfeature/sdk/EventDetails.java @@ -1,11 +1,13 @@ package dev.openfeature.sdk; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.experimental.SuperBuilder; /** * The details of a particular event. */ +@EqualsAndHashCode(callSuper = true) @Data @SuperBuilder(toBuilder = true) public class EventDetails extends ProviderEventDetails { diff --git a/src/main/java/dev/openfeature/sdk/EventProvider.java b/src/main/java/dev/openfeature/sdk/EventProvider.java index e9cdae55b..0d7e897c2 100644 --- a/src/main/java/dev/openfeature/sdk/EventProvider.java +++ b/src/main/java/dev/openfeature/sdk/EventProvider.java @@ -1,6 +1,10 @@ package dev.openfeature.sdk; import dev.openfeature.sdk.internal.TriConsumer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; /** * Abstract EventProvider. Providers must extend this class to support events. @@ -14,8 +18,10 @@ * * @see FeatureProvider */ +@Slf4j public abstract class EventProvider implements FeatureProvider { private EventProviderListener eventProviderListener; + private final ExecutorService emitterExecutor = Executors.newCachedThreadPool(); void setEventProviderListener(EventProviderListener eventProviderListener) { this.eventProviderListener = eventProviderListener; @@ -46,19 +52,56 @@ void detach() { this.onEmit = null; } + /** + * Stop the event emitter executor and block until either termination has completed + * or timeout period has elapsed. + */ + @Override + public void shutdown() { + emitterExecutor.shutdown(); + try { + if (!emitterExecutor.awaitTermination(EventSupport.SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.warn("Emitter executor did not terminate before the timeout period had elapsed"); + emitterExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + emitterExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + /** * Emit the specified {@link ProviderEvent}. * * @param event The event type * @param details The details of the event */ - public void emit(ProviderEvent event, ProviderEventDetails details) { - if (eventProviderListener != null) { - eventProviderListener.onEmit(event, details); - } - if (this.onEmit != null) { - this.onEmit.accept(this, event, details); + public Awaitable emit(final ProviderEvent event, final ProviderEventDetails details) { + final var localEventProviderListener = this.eventProviderListener; + final var localOnEmit = this.onEmit; + + if (localEventProviderListener == null && localOnEmit == null) { + return Awaitable.FINISHED; } + + final var awaitable = new Awaitable(); + + // These calls need to be executed on a different thread to prevent deadlocks when the provider initialization + // relies on a ready event to be emitted + emitterExecutor.submit(() -> { + try (var ignored = OpenFeatureAPI.lock.readLockAutoCloseable()) { + if (localEventProviderListener != null) { + localEventProviderListener.onEmit(event, details); + } + if (localOnEmit != null) { + localOnEmit.accept(this, event, details); + } + } finally { + awaitable.wakeup(); + } + }); + + return awaitable; } /** @@ -67,8 +110,8 @@ public void emit(ProviderEvent event, ProviderEventDetails details) { * * @param details The details of the event */ - public void emitProviderReady(ProviderEventDetails details) { - emit(ProviderEvent.PROVIDER_READY, details); + public Awaitable emitProviderReady(ProviderEventDetails details) { + return emit(ProviderEvent.PROVIDER_READY, details); } /** @@ -78,8 +121,8 @@ public void emitProviderReady(ProviderEventDetails details) { * * @param details The details of the event */ - public void emitProviderConfigurationChanged(ProviderEventDetails details) { - emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); + public Awaitable emitProviderConfigurationChanged(ProviderEventDetails details) { + return emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); } /** @@ -88,8 +131,8 @@ public void emitProviderConfigurationChanged(ProviderEventDetails details) { * * @param details The details of the event */ - public void emitProviderStale(ProviderEventDetails details) { - emit(ProviderEvent.PROVIDER_STALE, details); + public Awaitable emitProviderStale(ProviderEventDetails details) { + return emit(ProviderEvent.PROVIDER_STALE, details); } /** @@ -98,7 +141,7 @@ public void emitProviderStale(ProviderEventDetails details) { * * @param details The details of the event */ - public void emitProviderError(ProviderEventDetails details) { - emit(ProviderEvent.PROVIDER_ERROR, details); + public Awaitable emitProviderError(ProviderEventDetails details) { + return emit(ProviderEvent.PROVIDER_ERROR, details); } } diff --git a/src/main/java/dev/openfeature/sdk/EventSupport.java b/src/main/java/dev/openfeature/sdk/EventSupport.java index d3af45991..8396795bd 100644 --- a/src/main/java/dev/openfeature/sdk/EventSupport.java +++ b/src/main/java/dev/openfeature/sdk/EventSupport.java @@ -1,12 +1,12 @@ package dev.openfeature.sdk; -import java.util.ArrayList; -import java.util.List; +import java.util.Collection; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -19,17 +19,14 @@ @Slf4j class EventSupport { + public static final int SHUTDOWN_TIMEOUT_SECONDS = 3; + // we use a v4 uuid as a "placeholder" for anonymous clients, since // ConcurrentHashMap doesn't support nulls - private static final String defaultClientUuid = UUID.randomUUID().toString(); - private static final int SHUTDOWN_TIMEOUT_SECONDS = 3; + private static final String DEFAULT_CLIENT_UUID = UUID.randomUUID().toString(); private final Map handlerStores = new ConcurrentHashMap<>(); private final HandlerStore globalHandlerStore = new HandlerStore(); - private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> { - final Thread thread = new Thread(runnable); - thread.setDaemon(true); - return thread; - }); + private final ExecutorService taskExecutor = Executors.newCachedThreadPool(); /** * Run all the event handlers associated with this domain. @@ -40,11 +37,10 @@ class EventSupport { * @param eventDetails the event details */ public void runClientHandlers(String domain, ProviderEvent event, EventDetails eventDetails) { - domain = Optional.ofNullable(domain).orElse(defaultClientUuid); + domain = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID); // run handlers if they exist Optional.ofNullable(handlerStores.get(domain)) - .filter(store -> Optional.of(store).isPresent()) .map(store -> store.handlerMap.get(event)) .ifPresent(handlers -> handlers.forEach(handler -> runHandler(handler, eventDetails))); } @@ -69,7 +65,7 @@ public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) { * @param handler the handler function to run */ public void addClientHandler(String domain, ProviderEvent event, Consumer handler) { - final String name = Optional.ofNullable(domain).orElse(defaultClientUuid); + final String name = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID); // lazily create and cache a HandlerStore if it doesn't exist HandlerStore store = Optional.ofNullable(this.handlerStores.get(name)).orElseGet(() -> { @@ -89,7 +85,7 @@ public void addClientHandler(String domain, ProviderEvent event, Consumer handler) { - domain = Optional.ofNullable(domain).orElse(defaultClientUuid); + domain = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID); this.handlerStores.get(domain).removeHandler(event, handler); } @@ -160,14 +156,14 @@ public void shutdown() { // instantiated when a handler is added to that client. static class HandlerStore { - private final Map>> handlerMap; + private final Map>> handlerMap; HandlerStore() { handlerMap = new ConcurrentHashMap<>(); - handlerMap.put(ProviderEvent.PROVIDER_READY, new ArrayList<>()); - handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ArrayList<>()); - handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ArrayList<>()); - handlerMap.put(ProviderEvent.PROVIDER_STALE, new ArrayList<>()); + handlerMap.put(ProviderEvent.PROVIDER_READY, new ConcurrentLinkedQueue<>()); + handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ConcurrentLinkedQueue<>()); + handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ConcurrentLinkedQueue<>()); + handlerMap.put(ProviderEvent.PROVIDER_STALE, new ConcurrentLinkedQueue<>()); } void addHandler(ProviderEvent event, Consumer handler) { diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/src/main/java/dev/openfeature/sdk/FeatureProvider.java index 4c630cb80..22819ef10 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProvider.java @@ -30,6 +30,7 @@ default List getProviderHooks() { * can overwrite this method, * if they have special initialization needed prior being called for flag * evaluation. + * *

* It is ok if the method is expensive as it is executed in the background. All * runtime exceptions will be @@ -45,6 +46,7 @@ default void initialize(EvaluationContext evaluationContext) throws Exception { * flags, or the SDK is shut down. * Providers can overwrite this method, if they have special shutdown actions * needed. + * *

* It is ok if the method is expensive as it is executed in the background. All * runtime exceptions will be diff --git a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java index 2c39ece6b..5fd70221b 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java @@ -2,14 +2,14 @@ import dev.openfeature.sdk.exceptions.OpenFeatureError; import java.util.concurrent.atomic.AtomicBoolean; -import lombok.Getter; +import java.util.concurrent.atomic.AtomicReference; +import lombok.extern.slf4j.Slf4j; +@Slf4j class FeatureProviderStateManager implements EventProviderListener { private final FeatureProvider delegate; private final AtomicBoolean isInitialized = new AtomicBoolean(); - - @Getter - private ProviderState state = ProviderState.NOT_READY; + private final AtomicReference state = new AtomicReference<>(ProviderState.NOT_READY); public FeatureProviderStateManager(FeatureProvider delegate) { this.delegate = delegate; @@ -24,17 +24,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } try { delegate.initialize(evaluationContext); - state = ProviderState.READY; + setState(ProviderState.READY); } catch (OpenFeatureError openFeatureError) { if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) { - state = ProviderState.FATAL; + setState(ProviderState.FATAL); } else { - state = ProviderState.ERROR; + setState(ProviderState.ERROR); } isInitialized.set(false); throw openFeatureError; } catch (Exception e) { - state = ProviderState.ERROR; + setState(ProviderState.ERROR); isInitialized.set(false); throw e; } @@ -42,7 +42,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { public void shutdown() { delegate.shutdown(); - state = ProviderState.NOT_READY; + setState(ProviderState.NOT_READY); isInitialized.set(false); } @@ -50,17 +50,34 @@ public void shutdown() { public void onEmit(ProviderEvent event, ProviderEventDetails details) { if (ProviderEvent.PROVIDER_ERROR.equals(event)) { if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) { - state = ProviderState.FATAL; + setState(ProviderState.FATAL); } else { - state = ProviderState.ERROR; + setState(ProviderState.ERROR); } } else if (ProviderEvent.PROVIDER_STALE.equals(event)) { - state = ProviderState.STALE; + setState(ProviderState.STALE); } else if (ProviderEvent.PROVIDER_READY.equals(event)) { - state = ProviderState.READY; + setState(ProviderState.READY); + } + } + + private void setState(ProviderState state) { + ProviderState oldState = this.state.getAndSet(state); + if (oldState != state) { + String providerName; + if (delegate.getMetadata() == null || delegate.getMetadata().getName() == null) { + providerName = "unknown"; + } else { + providerName = delegate.getMetadata().getName(); + } + log.info("Provider {} transitioned from state {} to state {}", providerName, oldState, state); } } + public ProviderState getState() { + return state.get(); + } + FeatureProvider getProvider() { return delegate; } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index 23a452e08..8560c369e 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.experimental.Delegate; @@ -15,6 +16,7 @@ * not be modified after instantiation. */ @ToString +@EqualsAndHashCode @SuppressWarnings("PMD.BeanMembersShouldSerialize") public final class ImmutableContext implements EvaluationContext { diff --git a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java index c2b6f5838..7f57a174d 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java @@ -97,6 +97,14 @@ public T getValue(final String key, final Class type) { } } + public boolean isEmpty() { + return metadata.isEmpty(); + } + + public boolean isNotEmpty() { + return !metadata.isEmpty(); + } + /** * Obtain a builder for {@link ImmutableMetadata}. */ diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index c47a49eb3..849359424 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -18,7 +18,7 @@ * not be modified after instantiation. All references are clones. */ @ToString -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) @SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) public final class ImmutableStructure extends AbstractStructure { @@ -38,7 +38,7 @@ public ImmutableStructure(Map attributes) { super(copyAttributes(attributes, null)); } - protected ImmutableStructure(String targetingKey, Map attributes) { + ImmutableStructure(String targetingKey, Map attributes) { super(copyAttributes(attributes, targetingKey)); } @@ -70,12 +70,14 @@ private static Map copyAttributes(Map in) { private static Map copyAttributes(Map in, String targetingKey) { Map copy = new HashMap<>(); - for (Entry entry : in.entrySet()) { - copy.put( - entry.getKey(), - Optional.ofNullable(entry.getValue()) - .map((Value val) -> val.clone()) - .orElse(null)); + if (in != null) { + for (Entry entry : in.entrySet()) { + copy.put( + entry.getKey(), + Optional.ofNullable(entry.getValue()) + .map((Value val) -> val.clone()) + .orElse(null)); + } } if (targetingKey != null) { copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey)); diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index a06e2f2d3..f3158456d 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -15,8 +15,8 @@ * be modified after instantiation. */ @ToString -@EqualsAndHashCode @SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) +@EqualsAndHashCode(callSuper = true) public class MutableStructure extends AbstractStructure { public MutableStructure() { diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 9175a7cd7..66d40736f 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -29,7 +29,7 @@ public class OpenFeatureAPI implements EventBus { protected OpenFeatureAPI() { apiHooks = new ArrayList<>(); - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); transactionContextPropagator = new NoOpTransactionContextPropagator(); } @@ -207,7 +207,13 @@ public void setProvider(String domain, FeatureProvider provider) { } /** - * Set the default provider and wait for initialization to finish. + * Sets the default provider and waits for its initialization to complete. + * + *

Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown. + * It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully. + * + * @param provider the {@link FeatureProvider} to set as the default. + * @throws OpenFeatureError if the provider fails during initialization. */ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { @@ -224,8 +230,12 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError /** * Add a provider for a domain and wait for initialization to finish. * + *

Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown. + * It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully. + * * @param domain The domain to bind the provider to. * @param provider The provider to set. + * @throws OpenFeatureError if the provider fails during initialization. */ public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { @@ -333,7 +343,7 @@ public void shutdown() { providerRepository.shutdown(); eventSupport.shutdown(); - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); } } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 66f25f60a..e68d28f79 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -178,12 +178,6 @@ private FlagEvaluationDetails evaluateFlag( // provider must be accessed once to maintain a consistent reference provider = stateManager.getProvider(); ProviderState state = stateManager.getState(); - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError("provider not yet initialized"); - } - if (ProviderState.FATAL.equals(state)) { - throw new FatalError("provider is in an irrecoverable error state"); - } mergedHooks = ObjectUtils.merge( provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); @@ -203,6 +197,14 @@ private FlagEvaluationDetails evaluateFlag( afterHookContext = HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), mergedCtx, defaultValue); + // "short circuit" if the provider is in NOT_READY or FATAL state + if (ProviderState.NOT_READY.equals(state)) { + throw new ProviderNotReadyError("Provider not yet initialized"); + } + if (ProviderState.FATAL.equals(state)) { + throw new FatalError("Provider is in an irrecoverable error state"); + } + ProviderEvaluation providerEval = (ProviderEvaluation) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx); @@ -217,7 +219,7 @@ private FlagEvaluationDetails evaluateFlag( } } catch (Exception e) { if (details == null) { - details = FlagEvaluationDetails.builder().build(); + details = FlagEvaluationDetails.builder().flagKey(key).build(); } if (e instanceof OpenFeatureError) { details.setErrorCode(((OpenFeatureError) e).getErrorCode()); @@ -507,7 +509,7 @@ public Client onProviderStale(Consumer handler) { */ @Override public Client on(ProviderEvent event, Consumer handler) { - OpenFeatureAPI.getInstance().addHandler(domain, event, handler); + openfeatureApi.addHandler(domain, event, handler); return this; } @@ -516,7 +518,7 @@ public Client on(ProviderEvent event, Consumer handler) { */ @Override public Client removeHandler(ProviderEvent event, Consumer handler) { - OpenFeatureAPI.getInstance().removeHandler(domain, event, handler); + openfeatureApi.removeHandler(domain, event, handler); return this; } } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index bec866820..ab024a750 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -28,6 +28,11 @@ class ProviderRepository { return thread; }); private final Object registerStateManagerLock = new Object(); + private final OpenFeatureAPI openFeatureAPI; + + public ProviderRepository(OpenFeatureAPI openFeatureAPI) { + this.openFeatureAPI = openFeatureAPI; + } FeatureProviderStateManager getFeatureProviderStateManager() { return defaultStateManger.get(); @@ -205,7 +210,7 @@ private void initializeProvider( FeatureProviderStateManager oldManager) { try { if (ProviderState.NOT_READY.equals(newManager.getState())) { - newManager.initialize(OpenFeatureAPI.getInstance().getEvaluationContext()); + newManager.initialize(openFeatureAPI.getEvaluationContext()); afterInit.accept(newManager.getProvider()); } shutDownOld(oldManager, afterShutdown); diff --git a/src/main/java/dev/openfeature/sdk/Telemetry.java b/src/main/java/dev/openfeature/sdk/Telemetry.java new file mode 100644 index 000000000..7e94983ee --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/Telemetry.java @@ -0,0 +1,95 @@ +package dev.openfeature.sdk; + +/** + * The Telemetry class provides constants and methods for creating OpenTelemetry compliant + * evaluation events. + */ +public class Telemetry { + + private Telemetry() {} + + /* + The OpenTelemetry compliant event attributes for flag evaluation. + Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ + */ + public static final String TELEMETRY_KEY = "feature_flag.key"; + public static final String TELEMETRY_ERROR_CODE = "error.type"; + public static final String TELEMETRY_VARIANT = "feature_flag.result.variant"; + public static final String TELEMETRY_VALUE = "feature_flag.result.value"; + public static final String TELEMETRY_CONTEXT_ID = "feature_flag.context.id"; + public static final String TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message"; + public static final String TELEMETRY_REASON = "feature_flag.result.reason"; + public static final String TELEMETRY_PROVIDER = "feature_flag.provider.name"; + public static final String TELEMETRY_FLAG_SET_ID = "feature_flag.set.id"; + public static final String TELEMETRY_VERSION = "feature_flag.version"; + + // Well-known flag metadata attributes for telemetry events. + // Specification: https://openfeature.dev/specification/appendix-d#flag-metadata + public static final String TELEMETRY_FLAG_META_CONTEXT_ID = "contextId"; + public static final String TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId"; + public static final String TELEMETRY_FLAG_META_VERSION = "version"; + + public static final String FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation"; + + /** + * Creates an EvaluationEvent using the provided HookContext and ProviderEvaluation. + * + * @param hookContext the context containing flag evaluation details + * @param evaluationDetails the evaluation result from the provider + * + * @return an EvaluationEvent populated with telemetry data + */ + public static EvaluationEvent createEvaluationEvent( + HookContext hookContext, FlagEvaluationDetails evaluationDetails) { + EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder() + .name(FLAG_EVALUATION_EVENT_NAME) + .attribute(TELEMETRY_KEY, hookContext.getFlagKey()) + .attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName()); + + if (evaluationDetails.getReason() != null) { + evaluationEventBuilder.attribute( + TELEMETRY_REASON, evaluationDetails.getReason().toLowerCase()); + } else { + evaluationEventBuilder.attribute( + TELEMETRY_REASON, Reason.UNKNOWN.name().toLowerCase()); + } + + if (evaluationDetails.getVariant() != null) { + evaluationEventBuilder.attribute(TELEMETRY_VARIANT, evaluationDetails.getVariant()); + } else { + evaluationEventBuilder.attribute(TELEMETRY_VALUE, evaluationDetails.getValue()); + } + + String contextId = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_CONTEXT_ID); + if (contextId != null) { + evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, contextId); + } else { + evaluationEventBuilder.attribute( + TELEMETRY_CONTEXT_ID, hookContext.getCtx().getTargetingKey()); + } + + String setID = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_FLAG_SET_ID); + if (setID != null) { + evaluationEventBuilder.attribute(TELEMETRY_FLAG_SET_ID, setID); + } + + String version = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_VERSION); + if (version != null) { + evaluationEventBuilder.attribute(TELEMETRY_VERSION, version); + } + + if (Reason.ERROR.name().equals(evaluationDetails.getReason())) { + if (evaluationDetails.getErrorCode() != null) { + evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, evaluationDetails.getErrorCode()); + } else { + evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, ErrorCode.GENERAL); + } + + if (evaluationDetails.getErrorMessage() != null) { + evaluationEventBuilder.attribute(TELEMETRY_ERROR_MSG, evaluationDetails.getErrorMessage()); + } + } + + return evaluationEventBuilder.build(); + } +} diff --git a/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java index 15b0208c0..484672d8a 100644 --- a/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java +++ b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java @@ -1,6 +1,14 @@ package dev.openfeature.sdk; +import java.util.Optional; + /** * Data pertinent to a particular tracking event. */ -public interface TrackingEventDetails extends Structure {} +public interface TrackingEventDetails extends Structure { + + /** + * Returns the optional numeric tracking value. + */ + Optional getValue(); +} diff --git a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java index 05f7d3eb8..9e2718787 100644 --- a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java +++ b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java @@ -5,6 +5,7 @@ * for the duration of a single transaction. * Examples of potential transaction specific context include: a user id, user agent, IP. * Transaction context is merged with evaluation context prior to flag evaluation. + * *

* The precedence of merging context can be seen in * the specification. diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java index 61778d85b..f2dc6b495 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk.providers.memory; +import dev.openfeature.sdk.ImmutableMetadata; import java.util.Map; import lombok.Builder; import lombok.Getter; @@ -18,4 +19,5 @@ public class Flag { private String defaultVariant; private ContextEvaluator contextEvaluator; + private ImmutableMetadata flagMetadata; } diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index d3fdb985c..3be1b6316 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -152,6 +152,7 @@ private ProviderEvaluation getEvaluation( .value(value) .variant(flag.getDefaultVariant()) .reason(Reason.STATIC.toString()) + .flagMetadata(flag.getFlagMetadata()) .build(); } } diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java index 8f304eaac..bd0ac2c21 100644 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java @@ -1,14 +1,12 @@ package dev.openfeature.sdk; -import dev.openfeature.sdk.exceptions.FlagNotFoundError; - public class AlwaysBrokenWithDetailsProvider implements FeatureProvider { + private final String name = "always broken with details"; + @Override public Metadata getMetadata() { - return () -> { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - }; + return () -> name; } @Override diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java similarity index 94% rename from src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java rename to src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java index 2f214d8ac..0ad09db29 100644 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java @@ -2,7 +2,7 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; -public class AlwaysBrokenProvider implements FeatureProvider { +public class AlwaysBrokenWithExceptionProvider implements FeatureProvider { private final String name = "always broken"; diff --git a/src/test/java/dev/openfeature/sdk/AwaitableTest.java b/src/test/java/dev/openfeature/sdk/AwaitableTest.java new file mode 100644 index 000000000..70ef7902c --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/AwaitableTest.java @@ -0,0 +1,75 @@ +package dev.openfeature.sdk; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@Timeout(value = 5, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +class AwaitableTest { + @Test + void waitingForFinishedIsANoOp() { + var startTime = System.currentTimeMillis(); + Awaitable.FINISHED.await(); + var endTime = System.currentTimeMillis(); + assertTrue(endTime - startTime < 10); + } + + @Test + void waitingForNotFinishedWaitsEvenWhenInterrupted() throws InterruptedException { + var awaitable = new Awaitable(); + var mayProceed = new AtomicBoolean(false); + + var thread = new Thread(() -> { + awaitable.await(); + if (!mayProceed.get()) { + fail(); + } + }); + thread.start(); + + var startTime = System.currentTimeMillis(); + do { + thread.interrupt(); + } while (startTime + 1000 > System.currentTimeMillis()); + mayProceed.set(true); + awaitable.wakeup(); + thread.join(); + } + + @Test + void callingWakeUpWakesUpAllWaitingThreads() throws InterruptedException { + var awaitable = new Awaitable(); + var isRunning = new AtomicInteger(); + + Runnable runnable = () -> { + isRunning.incrementAndGet(); + var start = System.currentTimeMillis(); + awaitable.await(); + var end = System.currentTimeMillis(); + if (end - start > 10) { + fail(); + } + }; + + var numThreads = 2; + var threads = new Thread[numThreads]; + for (int i = 0; i < numThreads; i++) { + threads[i] = new Thread(runnable); + threads[i].start(); + } + + await().atMost(1, TimeUnit.SECONDS).until(() -> isRunning.get() == numThreads); + + awaitable.wakeup(); + + for (int i = 0; i < numThreads; i++) { + threads[i].join(); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java index cd7e8b295..beadf7aad 100644 --- a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java +++ b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java @@ -2,17 +2,16 @@ import static org.junit.jupiter.api.Assertions.*; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import org.junit.jupiter.api.Test; class ClientProviderMappingTest { @Test void clientProviderTest() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); - FeatureProviderTestUtils.setFeatureProvider("client1", new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider("client2", new NoOpProvider()); + api.setProviderAndWait("client1", new DoSomethingProvider()); + api.setProviderAndWait("client2", new NoOpProvider()); Client c1 = api.getClient("client1"); Client c2 = api.getClient("client2"); diff --git a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java index aacf09169..c954c8b19 100644 --- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java +++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.verify; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.Arrays; import java.util.HashMap; @@ -16,14 +15,20 @@ import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class DeveloperExperienceTest implements HookFixtures { transient String flagKey = "mykey"; + private OpenFeatureAPI api; + + @BeforeEach + public void setUp() throws Exception { + api = new OpenFeatureAPI(); + } @Test void simpleBooleanFlag() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Boolean retval = client.getBooleanValue(flagKey, false); @@ -34,7 +39,6 @@ void simpleBooleanFlag() { void clientHooks() { Hook exampleHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(exampleHook); @@ -48,7 +52,6 @@ void evalHooks() { Hook clientHook = mockBooleanHook(); Hook evalHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(clientHook); @@ -69,7 +72,6 @@ void evalHooks() { @Test void providingContext() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Map attributes = new HashMap<>(); @@ -86,8 +88,7 @@ void providingContext() { @Test void brokenProvider() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client client = api.getClient(); FlagEvaluationDetails retval = client.getBooleanDetails(flagKey, false); assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode()); @@ -99,6 +100,9 @@ void brokenProvider() { @Test void providerLockedPerTransaction() { + final String defaultValue = "string-value"; + final OpenFeatureAPI api = new OpenFeatureAPI(); + class MutatingHook implements Hook { @Override @@ -106,16 +110,14 @@ class MutatingHook implements Hook { // change the provider during a before hook - this should not impact the evaluation in progress public Optional before(HookContext ctx, Map hints) { - FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider()); return Optional.empty(); } } - final String defaultValue = "string-value"; - final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); api.addHooks(new MutatingHook()); // if provider is changed during an evaluation transaction it should proceed with the original provider @@ -132,7 +134,6 @@ public Optional before(HookContext ctx, Map hints) { @Test void setProviderAndWaitShouldPutTheProviderInReadyState() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(domain, new TestEventsProvider()); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); @@ -145,12 +146,11 @@ void setProviderAndWaitShouldPutTheProviderInReadyState() { @Test void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); - provider.emitProviderError(ProviderEventDetails.builder().build()); + provider.emitProviderError(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR); } @@ -161,12 +161,11 @@ void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { @Test void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); - provider.emitProviderStale(ProviderEventDetails.builder().build()); + provider.emitProviderStale(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); } @@ -177,14 +176,13 @@ void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { @Test void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); - provider.emitProviderStale(ProviderEventDetails.builder().build()); + provider.emitProviderStale(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); - provider.emitProviderReady(ProviderEventDetails.builder().build()); + provider.emitProviderReady(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); } } diff --git a/src/test/java/dev/openfeature/sdk/EventProviderTest.java b/src/test/java/dev/openfeature/sdk/EventProviderTest.java index d8af6e8d3..d04fa88d1 100644 --- a/src/test/java/dev/openfeature/sdk/EventProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/EventProviderTest.java @@ -5,13 +5,18 @@ import static org.mockito.Mockito.*; import dev.openfeature.sdk.internal.TriConsumer; +import dev.openfeature.sdk.testutils.TestStackedEmitCallsProvider; +import io.cucumber.java.AfterAll; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; class EventProviderTest { + private static final int TIMEOUT = 300; + private TestEventProvider eventProvider; @BeforeEach @@ -21,7 +26,13 @@ void setup() { eventProvider.initialize(null); } + @AfterAll + public static void resetDefaultProvider() { + new OpenFeatureAPI().setProviderAndWait(new NoOpProvider()); + } + @Test + @Timeout(value = 2, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) @DisplayName("should run attached onEmit with emitters") void emitsEventsWhenAttached() { TriConsumer onEmit = mockOnEmit(); @@ -34,10 +45,10 @@ void emitsEventsWhenAttached() { eventProvider.emitProviderStale(details); eventProvider.emitProviderError(details); - verify(onEmit, times(2)).accept(eventProvider, ProviderEvent.PROVIDER_READY, details); - verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); - verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_STALE, details); - verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_ERROR, details); + verify(onEmit, timeout(TIMEOUT).times(2)).accept(eventProvider, ProviderEvent.PROVIDER_READY, details); + verify(onEmit, timeout(TIMEOUT)).accept(eventProvider, ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); + verify(onEmit, timeout(TIMEOUT)).accept(eventProvider, ProviderEvent.PROVIDER_STALE, details); + verify(onEmit, timeout(TIMEOUT)).accept(eventProvider, ProviderEvent.PROVIDER_ERROR, details); } @Test @@ -75,6 +86,15 @@ void doesNotThrowWhenOnEmitSame() { eventProvider.attach(onEmit2); // should not throw, same instance. noop } + @Test + @SneakyThrows + @Timeout(value = 2, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + @DisplayName("should not deadlock on emit called during emit") + void doesNotDeadlockOnEmitStackedCalls() { + TestStackedEmitCallsProvider provider = new TestStackedEmitCallsProvider(); + new OpenFeatureAPI().setProviderAndWait(provider); + } + static class TestEventProvider extends EventProvider { private static final String NAME = "TestEventProvider"; diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java index 02a5953b9..b232f1177 100644 --- a/src/test/java/dev/openfeature/sdk/EventsTest.java +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -7,11 +7,11 @@ import static org.mockito.Mockito.*; import dev.openfeature.sdk.testutils.TestEventsProvider; -import io.cucumber.java.AfterAll; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -19,12 +19,13 @@ class EventsTest { - private static final int TIMEOUT = 300; + private static final int TIMEOUT = 500; private static final int INIT_DELAY = TIMEOUT / 2; + private OpenFeatureAPI api; - @AfterAll - public static void resetDefaultProvider() { - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + @BeforeEach + void setUp() { + api = new OpenFeatureAPI(); } @Nested @@ -49,8 +50,8 @@ void apiInitReady() { final String name = "apiInitReady"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().onProviderReady(handler); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.onProviderReady(handler); + api.setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()).accept(any()); } @@ -66,8 +67,8 @@ void apiInitError() { final String errMessage = "oh no!"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - OpenFeatureAPI.getInstance().onProviderError(handler); - OpenFeatureAPI.getInstance().setProvider(name, provider); + api.onProviderError(handler); + api.setProvider(name, provider); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { return errMessage.equals(details.getMessage()); })); @@ -89,8 +90,8 @@ void apiShouldPropagateEvents() { final String name = "apiShouldPropagateEvents"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); + api.setProviderAndWait(name, provider); + api.onProviderConfigurationChanged(handler); provider.mockEvent( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, @@ -118,12 +119,12 @@ void apiShouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); - OpenFeatureAPI.getInstance().onProviderReady(handler1); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().onProviderStale(handler3); - OpenFeatureAPI.getInstance().onProviderError(handler4); + api.onProviderReady(handler1); + api.onProviderConfigurationChanged(handler2); + api.onProviderStale(handler3); + api.onProviderError(handler4); Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { provider.mockEvent( @@ -162,8 +163,8 @@ void shouldPropagateDefaultAndAnon() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - Client client = OpenFeatureAPI.getInstance().getClient(); + api.setProviderAndWait(provider); + Client client = api.getClient(); client.onProviderStale(handler); provider.mockEvent( @@ -183,8 +184,8 @@ void shouldPropagateDefaultAndNamed() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(provider); + Client client = api.getClient(name); client.onProviderStale(handler); provider.mockEvent( @@ -213,10 +214,10 @@ void initReadyProviderBefore() { final String name = "initReadyProviderBefore"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderReady(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(argThat(details -> details.getDomain().equals(name))); } @@ -233,8 +234,8 @@ void initReadyProviderAfter() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderReady(handler); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(argThat(details -> details.getDomain().equals(name))); @@ -252,10 +253,10 @@ void initErrorProviderAfter() { final String errMessage = "oh no!"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderError(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + api.setProvider(name, provider); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { return name.equals(details.getDomain()) && errMessage.equals(details.getMessage()); })); @@ -274,8 +275,8 @@ void initErrorProviderBefore() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProvider(name, provider); + Client client = api.getClient(name); client.onProviderError(handler); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { return name.equals(details.getDomain()) && errMessage.equals(details.getMessage()); @@ -299,8 +300,8 @@ void shouldPropagateBefore() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); provider.mockEvent( @@ -322,10 +323,10 @@ void shouldPropagateAfter() { final String name = "shouldPropagateAfter"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); provider.mockEvent( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, @@ -354,8 +355,8 @@ void shouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderReady(handler1); client.onProviderConfigurationChanged(handler2); @@ -384,14 +385,14 @@ void shouldNotRunHandlers() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider1); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + api.onProviderConfigurationChanged(handler1); client.onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); + api.setProviderAndWait(name, provider2); // wait for the new provider to be ready and make sure things are cleaned up. await().until(() -> provider1.isShutDown()); @@ -421,11 +422,11 @@ void otherClientHandlersShouldNotRun() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name1, provider1); - OpenFeatureAPI.getInstance().setProviderAndWait(name2, provider2); + api.setProviderAndWait(name1, provider1); + api.setProviderAndWait(name2, provider2); - Client client1 = OpenFeatureAPI.getInstance().getClient(name1); - Client client2 = OpenFeatureAPI.getInstance().getClient(name2); + Client client1 = api.getClient(name1); + Client client2 = api.getClient(name2); client1.onProviderConfigurationChanged(handlerToRun); client2.onProviderConfigurationChanged(handlerNotToRun); @@ -450,11 +451,11 @@ void boundShouldNotRunWithDefault() { TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + api.setProviderAndWait(defaultProvider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handlerNotToRun); - OpenFeatureAPI.getInstance().setProviderAndWait(name, namedProvider); + api.setProviderAndWait(name, namedProvider); // await the new provider to make sure the old one is shut down await().until(() -> namedProvider.getState().equals(ProviderState.READY)); @@ -465,7 +466,7 @@ void boundShouldNotRunWithDefault() { ProviderEventDetails.builder().build()); verify(handlerNotToRun, after(TIMEOUT).never()).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -479,9 +480,9 @@ void unboundShouldRunWithDefault() { final Consumer handlerToRun = mockHandler(); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + api.setProviderAndWait(defaultProvider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handlerToRun); // await the new provider to make sure the old one is shut down @@ -493,7 +494,7 @@ void unboundShouldRunWithDefault() { ProviderEventDetails.builder().build()); verify(handlerToRun, timeout(TIMEOUT)).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -509,9 +510,9 @@ void handlersRunIfOneThrows() { final Consumer lastHandler = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); - Client client1 = OpenFeatureAPI.getInstance().getClient(name); + Client client1 = api.getClient(name); client1.onProviderConfigurationChanged(errorHandler); client1.onProviderConfigurationChanged(nextHandler); @@ -537,11 +538,11 @@ void shouldHaveAllProperties() { final String name = "shouldHaveAllProperties"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + api.onProviderConfigurationChanged(handler1); client.onProviderConfigurationChanged(handler2); List flagsChanged = Arrays.asList("flag"); @@ -577,15 +578,15 @@ void shouldHaveAllProperties() { number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") void matchingReadyEventsMustRunImmediately() { - final String name = "matchingEventsMustRunImmediately"; + final String name = "matchingReadyEventsMustRunImmediately"; final Consumer handler = mockHandler(); // provider which is already ready TestEventsProvider provider = new TestEventsProvider(); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); // should run even thought handler was added after ready - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderReady(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -596,18 +597,17 @@ void matchingReadyEventsMustRunImmediately() { number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") void matchingStaleEventsMustRunImmediately() { - final String name = "matchingEventsMustRunImmediately"; + final String name = "matchingStaleEventsMustRunImmediately"; final Consumer handler = mockHandler(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already stale - TestEventsProvider provider = TestEventsProvider.newInitializedTestEventsProvider(); + TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); Client client = api.getClient(name); api.setProviderAndWait(name, provider); - provider.emitProviderStale(ProviderEventDetails.builder().build()); + provider.emitProviderStale(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); - // should run even thought handler was added after stale + // should run even though handler was added after stale client.onProviderStale(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -618,18 +618,18 @@ void matchingStaleEventsMustRunImmediately() { number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") void matchingErrorEventsMustRunImmediately() { - final String name = "matchingEventsMustRunImmediately"; + final String name = "matchingErrorEventsMustRunImmediately"; final Consumer handler = mockHandler(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already in error - TestEventsProvider provider = new TestEventsProvider(); + TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); Client client = api.getClient(name); api.setProviderAndWait(name, provider); - provider.emitProviderError(ProviderEventDetails.builder().build()); + provider.emitProviderError(ProviderEventDetails.builder().build()).await(); assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR); - // should run even thought handler was added after error + verify(handler, never()).accept(any()); + // should run even though handler was added after error client.onProviderError(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -644,8 +644,8 @@ void mustPersistAcrossChanges() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider1); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); provider1.mockEvent( @@ -657,7 +657,7 @@ void mustPersistAcrossChanges() { verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches)); // wait for the new provider to be ready. - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); + api.setProviderAndWait(name, provider2); // verify that with the new provider under the same name, the handler is called // again. @@ -681,14 +681,14 @@ void removedEventsShouldNotRun() { final Consumer handler2 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderStale(handler1); + api.onProviderStale(handler1); client.onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().removeHandler(ProviderEvent.PROVIDER_STALE, handler1); + api.removeHandler(ProviderEvent.PROVIDER_STALE, handler1); client.removeHandler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler2); // emit event diff --git a/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java b/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java new file mode 100644 index 000000000..9ebd24758 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java @@ -0,0 +1,45 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.GeneralError; + +public class FatalErrorProvider implements FeatureProvider { + + private final String name = "fatal"; + + @Override + public Metadata getMetadata() { + return () -> name; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + throw new FatalError(); // throw a fatal error on startup (this will cause the SDK to short circuit evaluations) + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + throw new GeneralError(TestConstants.BROKEN_MESSAGE); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + throw new GeneralError(TestConstants.BROKEN_MESSAGE); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + throw new GeneralError(TestConstants.BROKEN_MESSAGE); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + throw new GeneralError(TestConstants.BROKEN_MESSAGE); + } + + @Override + public ProviderEvaluation getObjectEvaluation( + String key, Value defaultValue, EvaluationContext invocationContext) { + throw new GeneralError(TestConstants.BROKEN_MESSAGE); + } +} diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index 2ad88d328..3b02b172d 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -9,7 +9,6 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.HashMap; import java.util.List; @@ -29,7 +28,7 @@ class FlagEvaluationSpecTest implements HookFixtures { private OpenFeatureAPI api; private Client _client() { - FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); return api.getClient(); } @@ -37,18 +36,13 @@ private Client _client() { private Client _initializedClient() { TestEventsProvider provider = new TestEventsProvider(); provider.initialize(null); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); return api.getClient(); } @BeforeEach void getApiInstance() { - api = OpenFeatureAPI.getInstance(); - } - - @AfterEach - void reset_ctx() { - api.setEvaluationContext(null); + api = new OpenFeatureAPI(); } @BeforeEach @@ -62,15 +56,6 @@ void reset_logs() { LoggerMock.setMock(OpenFeatureClient.class, logger); } - @Specification( - number = "1.1.1", - text = - "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.") - @Test - void global_singleton() { - assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance()); - } - @Specification( number = "1.1.2.1", text = @@ -78,7 +63,7 @@ void global_singleton() { @Test void provider() { FeatureProvider mockProvider = mock(FeatureProvider.class); - FeatureProviderTestUtils.setFeatureProvider(mockProvider); + api.setProviderAndWait(mockProvider); assertThat(api.getProvider()).isEqualTo(mockProvider); } @@ -90,13 +75,13 @@ void provider() { @Test void providerAndWait() { FeatureProvider provider = new TestEventsProvider(500); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); provider = new TestEventsProvider(500); String providerName = "providerAndWait"; - OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider); + api.setProviderAndWait(providerName, provider); Client client2 = api.getClient(providerName); assertThat(client2.getProviderState()).isEqualTo(ProviderState.READY); } @@ -124,8 +109,8 @@ void providerAndWaitError() { void shouldReturnNotReadyIfNotInitialized() { FeatureProvider provider = new TestEventsProvider(100); String providerName = "shouldReturnNotReadyIfNotInitialized"; - OpenFeatureAPI.getInstance().setProvider(providerName, provider); - Client client = OpenFeatureAPI.getInstance().getClient(providerName); + api.setProvider(providerName, provider); + Client client = api.getClient(providerName); FlagEvaluationDetails details = client.getBooleanDetails("return_error_when_not_initialized", false); assertEquals(ErrorCode.PROVIDER_NOT_READY, details.getErrorCode()); assertEquals(Reason.ERROR.toString(), details.getReason()); @@ -136,7 +121,7 @@ void shouldReturnNotReadyIfNotInitialized() { text = "The API MUST provide a function for retrieving the metadata field of the configured provider.") @Test void provider_metadata() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name); } @@ -198,7 +183,7 @@ void hookRegistration() { "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.") @Test void value_flags() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; @@ -279,7 +264,7 @@ void value_flags() { "In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.") @Test void detail_flags() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; @@ -386,7 +371,7 @@ void hooks() { "In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") @Test void broken_provider() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -414,8 +399,8 @@ void broken_provider() { text = "In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") @Test - void broken_provider_withDetails() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenWithDetailsProvider()); + void broken_provider_withDetails() throws InterruptedException { + api.setProviderAndWait(new AlwaysBrokenWithDetailsProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -431,7 +416,7 @@ void broken_provider_withDetails() { text = "Methods, functions, or operations on the client SHOULD NOT write log messages.") @Test void log_on_error() throws NotImplementedException { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); @@ -450,7 +435,7 @@ void clientMetadata() { assertNull(c.getMetadata().getDomain()); String domainName = "test domain"; - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c2 = api.getClient(domainName); assertEquals(domainName, c2.getMetadata().getName()); @@ -463,7 +448,7 @@ void clientMetadata() { "In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.") @Test void reason_is_error_when_there_are_errors() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertEquals(Reason.ERROR.toString(), result.getReason()); @@ -475,7 +460,7 @@ void reason_is_error_when_there_are_errors() { "If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.") @Test void flag_metadata_passed() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null)); + api.setProviderAndWait(new DoSomethingProvider(null)); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertNotNull(result.getFlagMetadata()); @@ -487,7 +472,7 @@ void api_context() { String contextKey = "some-key"; String contextValue = "some-value"; DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Map attributes = new HashMap<>(); attributes.put(contextKey, new Value(contextValue)); @@ -514,7 +499,7 @@ void api_context() { @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); Hook hook = spy(new Hook() { @@ -702,7 +687,7 @@ public void after( @Test void setting_transaction_context_propagator() { DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); @@ -716,7 +701,7 @@ void setting_transaction_context_propagator() { @Test void setting_transaction_context() { DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); diff --git a/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java b/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java index f8b9ba58e..22912661f 100644 --- a/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagMetadataTest.java @@ -1,6 +1,8 @@ package dev.openfeature.sdk; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +11,7 @@ class FlagMetadataTest { @Test @DisplayName("Test metadata payload construction and retrieval") - public void builder_validation() { + void builder_validation() { // given ImmutableMetadata flagMetadata = ImmutableMetadata.builder() .addString("string", "string") @@ -42,7 +44,7 @@ public void builder_validation() { @Test @DisplayName("Value type mismatch returns a null") - public void value_type_validation() { + void value_type_validation() { // given ImmutableMetadata flagMetadata = ImmutableMetadata.builder().addString("string", "string").build(); @@ -53,11 +55,34 @@ public void value_type_validation() { @Test @DisplayName("A null is returned if key does not exist") - public void notfound_error_validation() { + void notfound_error_validation() { // given ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); // then assertThat(flagMetadata.getBoolean("string")).isNull(); } + + @Test + @DisplayName("isEmpty and isNotEmpty return correctly when the metadata is empty") + void isEmpty_isNotEmpty_return_correctly_when_metadata_is_empty() { + // given + ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); + + // then + assertTrue(flagMetadata.isEmpty()); + assertFalse(flagMetadata.isNotEmpty()); + } + + @Test + @DisplayName("isEmpty and isNotEmpty return correctly when the metadata is not empty") + void isEmpty_isNotEmpty_return_correctly_when_metadata_is_not_empty() { + // given + ImmutableMetadata flagMetadata = + ImmutableMetadata.builder().addString("a", "b").build(); + + // then + assertFalse(flagMetadata.isEmpty()); + assertTrue(flagMetadata.isNotEmpty()); + } } diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index d6247c649..3a953d18a 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -18,7 +18,6 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.ArrayList; import java.util.Arrays; @@ -28,16 +27,18 @@ import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; class HookSpecTest implements HookFixtures { - @AfterEach - void emptyApiHooks() { - // it's a singleton. Don't pollute each test. - OpenFeatureAPI.getInstance().clearHooks(); + + private OpenFeatureAPI api; + + @BeforeEach + void setUp() { + this.api = new OpenFeatureAPI(); } @Specification( @@ -163,7 +164,7 @@ void optional_properties() { .type(FlagValueType.INTEGER) .ctx(new ImmutableContext()) .defaultValue(1) - .clientMetadata(OpenFeatureAPI.getInstance().getClient().getMetadata()) + .clientMetadata(api.getClient().getMetadata()) .build(); } @@ -173,8 +174,8 @@ void optional_properties() { "The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.") @Test void before_runs_ahead_of_evaluation() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new AlwaysBrokenProvider()); + + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client client = api.getClient(); Hook evalHook = mockBooleanHook(); @@ -216,8 +217,7 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() { .errorMessage(errorMessage) .build()); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider("errorHookMustRun", provider); + api.setProviderAndWait("errorHookMustRun", provider); Client client = api.getClient("errorHookMustRun"); client.getBooleanValue( "key", @@ -259,7 +259,7 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() { @Test void hook_eval_order() { List evalOrder = new ArrayList<>(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait("evalOrder", new TestEventsProvider() { public List getProviderHooks() { return Collections.singletonList(new BooleanHook() { @@ -411,8 +411,7 @@ void error_stops_before() { doThrow(RuntimeException.class).when(h).before(any(), any()); Hook h2 = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); c.getBooleanDetails( @@ -516,8 +515,7 @@ void flag_eval_hook_order() { .thenReturn(ProviderEvaluation.builder().value(true).build()); InOrder order = inOrder(hook, provider); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); client.getBooleanValue( "key", @@ -596,6 +594,25 @@ void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() { assertThat(evaluationDetails.getValue()).isTrue(); } + @Test + void shortCircuit_flagResolution_runsHooksWithAllFields() { + String domain = "shortCircuit_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails"; + api.setProvider(domain, new FatalErrorProvider()); + + Hook hook = mockBooleanHook(); + String flagKey = "test-flag-key"; + Client client = api.getClient(domain); + client.getBooleanValue( + flagKey, + true, + new ImmutableContext(), + FlagEvaluationOptions.builder().hook(hook).build()); + + verify(hook).before(any(), any()); + verify(hook).error(any(HookContext.class), any(Exception.class), any(Map.class)); + verify(hook).finallyAfter(any(HookContext.class), any(FlagEvaluationDetails.class), any(Map.class)); + } + @Test void successful_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() { Hook hook = mockBooleanHook(); @@ -695,8 +712,7 @@ void mergeHappensCorrectly() { when(provider.getBooleanEvaluation(any(), any(), any())) .thenReturn(ProviderEvaluation.builder().value(true).build()); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); client.getBooleanValue( "key", @@ -761,11 +777,10 @@ void first_error_broken() { } private Client getClient(FeatureProvider provider) { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); if (provider == null) { - FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider()); } else { - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); } return api.getClient(); } diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java index e69a974b3..2b39be741 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java @@ -3,6 +3,7 @@ import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; @@ -133,4 +134,31 @@ void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() { Structure value = key1.asStructure(); assertArrayEquals(new Object[] {"key1_1"}, value.keySet().toArray()); } + + @DisplayName("Two different MutableContext objects with the different contents are not considered equal") + @Test + void unequalImmutableContextsAreNotEqual() { + final Map attributes = new HashMap<>(); + attributes.put("key1", new Value("val1")); + final ImmutableContext ctx = new ImmutableContext(attributes); + + final Map attributes2 = new HashMap<>(); + final ImmutableContext ctx2 = new ImmutableContext(attributes2); + + assertNotEquals(ctx, ctx2); + } + + @DisplayName("Two different MutableContext objects with the same content are considered equal") + @Test + void equalImmutableContextsAreEqual() { + final Map attributes = new HashMap<>(); + attributes.put("key1", new Value("val1")); + final ImmutableContext ctx = new ImmutableContext(attributes); + + final Map attributes2 = new HashMap<>(); + attributes2.put("key1", new Value("val1")); + final ImmutableContext ctx2 = new ImmutableContext(attributes2); + + assertEquals(ctx, ctx2); + } } diff --git a/src/test/java/dev/openfeature/sdk/ImmutableMetadataTest.java b/src/test/java/dev/openfeature/sdk/ImmutableMetadataTest.java new file mode 100644 index 000000000..e3bd03165 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/ImmutableMetadataTest.java @@ -0,0 +1,28 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +class ImmutableMetadataTest { + @Test + void unequalImmutableMetadataAreUnequal() { + ImmutableMetadata i1 = + ImmutableMetadata.builder().addString("key1", "value1").build(); + ImmutableMetadata i2 = + ImmutableMetadata.builder().addString("key1", "value2").build(); + + assertNotEquals(i1, i2); + } + + @Test + void equalImmutableMetadataAreEqual() { + ImmutableMetadata i1 = + ImmutableMetadata.builder().addString("key1", "value1").build(); + ImmutableMetadata i2 = + ImmutableMetadata.builder().addString("key1", "value1").build(); + + assertEquals(i1, i2); + } +} diff --git a/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java b/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java index dff95adca..6a0eed59b 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java @@ -1,6 +1,11 @@ package dev.openfeature.sdk; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -154,4 +159,42 @@ void constructorHandlesNullValue() { attrs.put("null", null); new ImmutableStructure(attrs); } + + @Test + void unequalImmutableStructuresAreNotEqual() { + Map attrs1 = new HashMap<>(); + attrs1.put("test", new Value(45)); + ImmutableStructure structure1 = new ImmutableStructure(attrs1); + + Map attrs2 = new HashMap<>(); + attrs2.put("test", new Value(2)); + ImmutableStructure structure2 = new ImmutableStructure(attrs2); + + assertNotEquals(structure1, structure2); + } + + @Test + void equalImmutableStructuresAreEqual() { + Map attrs1 = new HashMap<>(); + attrs1.put("test", new Value(45)); + ImmutableStructure structure1 = new ImmutableStructure(attrs1); + + Map attrs2 = new HashMap<>(); + attrs2.put("test", new Value(45)); + ImmutableStructure structure2 = new ImmutableStructure(attrs2); + + assertEquals(structure1, structure2); + } + + @Test + void emptyImmutableStructureIsEmpty() { + ImmutableStructure m1 = new ImmutableStructure(); + assertTrue(m1.isEmpty()); + } + + @Test + void immutableStructureWithNullAttributesIsEmpty() { + ImmutableStructure m1 = new ImmutableStructure(null); + assertTrue(m1.isEmpty()); + } } diff --git a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java index 3353f5644..4bcd73127 100644 --- a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java @@ -17,10 +17,12 @@ class InitializeBehaviorSpecTest { private static final String DOMAIN_NAME = "mydomain"; + private OpenFeatureAPI api; @BeforeEach void setupTest() { - OpenFeatureAPI.getInstance().setProvider(new NoOpProvider()); + this.api = new OpenFeatureAPI(); + api.setProvider(new NoOpProvider()); } @Nested @@ -37,7 +39,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagE FeatureProvider featureProvider = mock(FeatureProvider.class); doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); - OpenFeatureAPI.getInstance().setProvider(featureProvider); + api.setProvider(featureProvider); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -55,8 +57,7 @@ void shouldCatchExceptionThrownByTheProviderOnInitialization() throws Exception doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); doThrow(TestException.class).when(featureProvider).initialize(any()); - assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(featureProvider)) - .doesNotThrowAnyException(); + assertThatCode(() -> api.setProvider(featureProvider)).doesNotThrowAnyException(); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -77,7 +78,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItFor FeatureProvider featureProvider = mock(FeatureProvider.class); doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); - OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider); + api.setProvider(DOMAIN_NAME, featureProvider); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -95,8 +96,7 @@ void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() throws doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); doThrow(TestException.class).when(featureProvider).initialize(any()); - assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider)) - .doesNotThrowAnyException(); + assertThatCode(() -> api.setProvider(DOMAIN_NAME, featureProvider)).doesNotThrowAnyException(); verify(featureProvider, timeout(1000)).initialize(any()); } diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java similarity index 99% rename from src/test/java/dev/openfeature/sdk/LockingTest.java rename to src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java index 4b7af5530..ad86f4bc3 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.parallel.Isolated; @Isolated() -class LockingTest { +class LockingSingeltonTest { private static OpenFeatureAPI api; private OpenFeatureClient client; diff --git a/src/test/java/dev/openfeature/sdk/MutableContextTest.java b/src/test/java/dev/openfeature/sdk/MutableContextTest.java index 953e3f636..6c471d09a 100644 --- a/src/test/java/dev/openfeature/sdk/MutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/MutableContextTest.java @@ -3,6 +3,7 @@ import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; @@ -137,4 +138,31 @@ void shouldAllowChainingOfMutations() { assertEquals(2, context.getValue("key2").asInteger()); assertEquals(3.0, context.getValue("key3").asDouble()); } + + @DisplayName("Two different MutableContext objects with the different contents are not considered equal") + @Test + void unequalMutableContextsAreNotEqual() { + final Map attributes = new HashMap<>(); + attributes.put("key1", new Value("val1")); + final MutableContext ctx = new MutableContext(attributes); + + final Map attributes2 = new HashMap<>(); + final MutableContext ctx2 = new MutableContext(attributes2); + + assertNotEquals(ctx, ctx2); + } + + @DisplayName("Two different MutableContext objects with the same content are considered equal") + @Test + void equalMutableContextsAreEqual() { + final Map attributes = new HashMap<>(); + attributes.put("key1", new Value("val1")); + final MutableContext ctx = new MutableContext(attributes); + + final Map attributes2 = new HashMap<>(); + attributes2.put("key1", new Value("val1")); + final MutableContext ctx2 = new MutableContext(attributes2); + + assertEquals(ctx, ctx2); + } } diff --git a/src/test/java/dev/openfeature/sdk/MutableStructureTest.java b/src/test/java/dev/openfeature/sdk/MutableStructureTest.java new file mode 100644 index 000000000..ebd11af0d --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/MutableStructureTest.java @@ -0,0 +1,67 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class MutableStructureTest { + + @Test + void emptyMutableStructureIsEmpty() { + MutableStructure m1 = new MutableStructure(); + assertTrue(m1.isEmpty()); + } + + @Test + void mutableStructureWithNullBackingStructureIsEmpty() { + MutableStructure m1 = new MutableStructure(null); + assertTrue(m1.isEmpty()); + } + + @Test + void unequalMutableStructuresAreNotEqual() { + MutableStructure m1 = new MutableStructure(); + m1.add("key1", "val1"); + MutableStructure m2 = new MutableStructure(); + m2.add("key2", "val2"); + assertNotEquals(m1, m2); + } + + @Test + void equalMutableStructuresAreEqual() { + MutableStructure m1 = new MutableStructure(); + m1.add("key1", "val1"); + MutableStructure m2 = new MutableStructure(); + m2.add("key1", "val1"); + assertEquals(m1, m2); + } + + @Test + void equalAbstractStructuresOfDifferentTypesAreNotEqual() { + MutableStructure m1 = new MutableStructure(); + m1.add("key1", "val1"); + HashMap map = new HashMap<>(); + map.put("key1", new Value("val1")); + AbstractStructure m2 = new AbstractStructure(map) { + @Override + public Set keySet() { + return attributes.keySet(); + } + + @Override + public Value getValue(String key) { + return attributes.get(key); + } + + @Override + public Map asMap() { + return attributes; + } + }; + + assertNotEquals(m1, m2); + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java new file mode 100644 index 000000000..dd9916eed --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java @@ -0,0 +1,17 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +class OpenFeatureAPISingeltonTest { + + @Specification( + number = "1.1.1", + text = + "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.") + @Test + void global_singleton() { + assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance()); + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index 63145ecb6..66fd06d55 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -5,10 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import dev.openfeature.sdk.providers.memory.InMemoryProvider; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.Collections; import java.util.HashMap; @@ -23,13 +23,13 @@ class OpenFeatureAPITest { @BeforeEach void setupTest() { - api = OpenFeatureAPI.getInstance(); + api = new OpenFeatureAPI(); } @Test void namedProviderTest() { FeatureProvider provider = new NoOpProvider(); - FeatureProviderTestUtils.setFeatureProvider("namedProviderTest", provider); + api.setProviderAndWait("namedProviderTest", provider); assertThat(provider.getMetadata().getName()) .isEqualTo(api.getProviderMetadata("namedProviderTest").getName()); @@ -44,14 +44,10 @@ void namedProviderOverwrittenTest() { String domain = "namedProviderOverwrittenTest"; FeatureProvider provider1 = new NoOpProvider(); FeatureProvider provider2 = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(domain, provider1); - FeatureProviderTestUtils.setFeatureProvider(domain, provider2); - - assertThat(OpenFeatureAPI.getInstance() - .getProvider(domain) - .getMetadata() - .getName()) - .isEqualTo(DoSomethingProvider.name); + api.setProviderAndWait(domain, provider1); + api.setProviderAndWait(domain, provider2); + + assertThat(api.getProvider(domain).getMetadata().getName()).isEqualTo(DoSomethingProvider.name); } @Test @@ -60,17 +56,17 @@ void providerToMultipleNames() throws Exception { FeatureProvider noOpAsNonEventingProvider = new NoOpProvider(); // register same provider for multiple names & as default provider - OpenFeatureAPI.getInstance().setProviderAndWait(inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientA", inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientB", inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientC", noOpAsNonEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientD", noOpAsNonEventingProvider); - - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider()); - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientA")); - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientB")); - assertEquals(noOpAsNonEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientC")); - assertEquals(noOpAsNonEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientD")); + api.setProviderAndWait(inMemAsEventingProvider); + api.setProviderAndWait("clientA", inMemAsEventingProvider); + api.setProviderAndWait("clientB", inMemAsEventingProvider); + api.setProviderAndWait("clientC", noOpAsNonEventingProvider); + api.setProviderAndWait("clientD", noOpAsNonEventingProvider); + + assertEquals(inMemAsEventingProvider, api.getProvider()); + assertEquals(inMemAsEventingProvider, api.getProvider("clientA")); + assertEquals(inMemAsEventingProvider, api.getProvider("clientB")); + assertEquals(noOpAsNonEventingProvider, api.getProvider("clientC")); + assertEquals(noOpAsNonEventingProvider, api.getProvider("clientD")); } @Test @@ -101,26 +97,23 @@ void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception { String domain = "namedProviderOverwrittenTest"; FeatureProvider provider1 = new NoOpProvider(); FeatureProvider provider2 = new TestEventsProvider(); - FeatureProviderTestUtils.setFeatureProvider(domain, provider1); - FeatureProviderTestUtils.setFeatureProvider(domain, provider2); + api.setProviderAndWait(domain, provider1); + api.setProviderAndWait(domain, provider2); provider2.initialize(null); - assertThat(OpenFeatureAPI.getInstance().getClient(domain).getProviderState()) - .isEqualTo(ProviderState.READY); + assertThat(api.getClient(domain).getProviderState()).isEqualTo(ProviderState.READY); } @Test void featureProviderTrackIsCalled() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); - FeatureProviderTestUtils.setFeatureProvider(featureProvider); + api.setProviderAndWait(featureProvider); - OpenFeatureAPI.getInstance() - .getClient() - .track("track-event", new ImmutableContext(), new MutableTrackingEventDetails(22.2f)); + api.getClient().track("track-event", new ImmutableContext(), new MutableTrackingEventDetails(22.2f)); verify(featureProvider).initialize(any()); - verify(featureProvider).getMetadata(); + verify(featureProvider, times(2)).getMetadata(); verify(featureProvider).track(any(), any(), any()); } } diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java new file mode 100644 index 000000000..f33c5b4d7 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java @@ -0,0 +1,10 @@ +package dev.openfeature.sdk; + +public class OpenFeatureAPITestUtil { + + private OpenFeatureAPITestUtil() {} + + public static OpenFeatureAPI createAPI() { + return new OpenFeatureAPI(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index 4f4d32004..97a1417a1 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -38,7 +38,7 @@ void reset_logs() { @Test @DisplayName("should not throw exception if hook has different type argument than hookContext") void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); api.setProviderAndWait( "shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider()); Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext"); @@ -82,7 +82,7 @@ void setEvaluationContextShouldAllowChaining() { @DisplayName("Should not call evaluation methods when the provider has state FATAL") void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { FeatureProvider provider = new TestEventsProvider(100, true, "fake fatal", true); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState"); assertThrows( @@ -97,7 +97,7 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { @DisplayName("Should not call evaluation methods when the provider has state NOT_READY") void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() { FeatureProvider provider = new TestEventsProvider(5000); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); api.setProvider("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState", provider); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState"); FlagEvaluationDetails details = client.getBooleanDetails("key", true); diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java index 98652635d..7041df5c1 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java @@ -35,7 +35,7 @@ class ProviderRepositoryTest { @BeforeEach void setupTest() { - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(new OpenFeatureAPI()); } @Nested diff --git a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java index e7caf9274..1bb7d4b62 100644 --- a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java @@ -1,6 +1,5 @@ package dev.openfeature.sdk; -import static dev.openfeature.sdk.testutils.FeatureProviderTestUtils.setFeatureProvider; import static org.mockito.Mockito.*; import dev.openfeature.sdk.fixtures.ProviderFixture; @@ -15,9 +14,19 @@ class ShutdownBehaviorSpecTest { private String DOMAIN = "myDomain"; + private OpenFeatureAPI api; + + void setFeatureProvider(FeatureProvider featureProvider) { + api.setProviderAndWait(featureProvider); + } + + void setFeatureProvider(String domain, FeatureProvider featureProvider) { + api.setProviderAndWait(domain, featureProvider); + } @BeforeEach void resetFeatureProvider() { + api = new OpenFeatureAPI(); setFeatureProvider(new NoOpProvider()); } @@ -110,7 +119,6 @@ void mustShutdownAllProvidersOnShuttingDownApi() { FeatureProvider namedProvider = ProviderFixture.createMockedProvider(); setFeatureProvider(defaultProvider); setFeatureProvider(DOMAIN, namedProvider); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); synchronized (OpenFeatureAPI.class) { api.shutdown(); @@ -125,15 +133,14 @@ void mustShutdownAllProvidersOnShuttingDownApi() { @Test @DisplayName("once shutdown is complete, api must be ready to use again") void apiIsReadyToUseAfterShutdown() { - final OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance(); NoOpProvider p1 = new NoOpProvider(); - openFeatureAPI.setProvider(p1); + api.setProvider(p1); - openFeatureAPI.shutdown(); + api.shutdown(); NoOpProvider p2 = new NoOpProvider(); - openFeatureAPI.setProvider(p2); + api.setProvider(p2); } } } diff --git a/src/test/java/dev/openfeature/sdk/TelemetryTest.java b/src/test/java/dev/openfeature/sdk/TelemetryTest.java new file mode 100644 index 000000000..2752683b8 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/TelemetryTest.java @@ -0,0 +1,231 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; + +public class TelemetryTest { + + @Test + void testCreatesEvaluationEventWithMandatoryFields() { + // Arrange + String flagKey = "test-flag"; + String providerName = "test-provider"; + String reason = "static"; + + Metadata providerMetadata = mock(Metadata.class); + when(providerMetadata.getName()).thenReturn(providerName); + + HookContext hookContext = HookContext.builder() + .flagKey(flagKey) + .providerMetadata(providerMetadata) + .type(FlagValueType.BOOLEAN) + .defaultValue(false) + .ctx(new ImmutableContext()) + .build(); + + FlagEvaluationDetails evaluation = FlagEvaluationDetails.builder() + .reason(reason) + .value(true) + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation); + + assertEquals(Telemetry.FLAG_EVALUATION_EVENT_NAME, event.getName()); + assertEquals(flagKey, event.getAttributes().get(Telemetry.TELEMETRY_KEY)); + assertEquals(providerName, event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); + assertEquals(reason.toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON)); + } + + @Test + void testHandlesNullReason() { + // Arrange + String flagKey = "test-flag"; + String providerName = "test-provider"; + + Metadata providerMetadata = mock(Metadata.class); + when(providerMetadata.getName()).thenReturn(providerName); + + HookContext hookContext = HookContext.builder() + .flagKey(flagKey) + .providerMetadata(providerMetadata) + .type(FlagValueType.BOOLEAN) + .defaultValue(false) + .ctx(new ImmutableContext()) + .build(); + + FlagEvaluationDetails evaluation = FlagEvaluationDetails.builder() + .reason(null) + .value(true) + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, evaluation); + + assertEquals(Reason.UNKNOWN.name().toLowerCase(), event.getAttributes().get(Telemetry.TELEMETRY_REASON)); + } + + @Test + void testSetsVariantAttributeWhenVariantExists() { + HookContext hookContext = HookContext.builder() + .flagKey("testFlag") + .type(FlagValueType.STRING) + .defaultValue("default") + .ctx(mock(EvaluationContext.class)) + .clientMetadata(mock(ClientMetadata.class)) + .providerMetadata(mock(Metadata.class)) + .build(); + + FlagEvaluationDetails providerEvaluation = FlagEvaluationDetails.builder() + .variant("testVariant") + .flagMetadata(ImmutableMetadata.builder().build()) + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); + + assertEquals("testVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); + } + + @Test + void test_sets_value_in_body_when_variant_is_null() { + HookContext hookContext = HookContext.builder() + .flagKey("testFlag") + .type(FlagValueType.STRING) + .defaultValue("default") + .ctx(mock(EvaluationContext.class)) + .clientMetadata(mock(ClientMetadata.class)) + .providerMetadata(mock(Metadata.class)) + .build(); + + FlagEvaluationDetails providerEvaluation = FlagEvaluationDetails.builder() + .value("testValue") + .flagMetadata(ImmutableMetadata.builder().build()) + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); + + assertEquals("testValue", event.getAttributes().get(Telemetry.TELEMETRY_VALUE)); + } + + @Test + void testAllFieldsPopulated() { + EvaluationContext evaluationContext = mock(EvaluationContext.class); + when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); + + Metadata providerMetadata = mock(Metadata.class); + when(providerMetadata.getName()).thenReturn("realProviderName"); + + HookContext hookContext = HookContext.builder() + .flagKey("realFlag") + .type(FlagValueType.STRING) + .defaultValue("realDefault") + .ctx(evaluationContext) + .clientMetadata(mock(ClientMetadata.class)) + .providerMetadata(providerMetadata) + .build(); + + FlagEvaluationDetails providerEvaluation = FlagEvaluationDetails.builder() + .flagMetadata(ImmutableMetadata.builder() + .addString("contextId", "realContextId") + .addString("flagSetId", "realFlagSetId") + .addString("version", "realVersion") + .build()) + .reason(Reason.DEFAULT.name()) + .variant("realVariant") + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); + + assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); + assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); + assertEquals("default", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); + assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); + assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); + assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); + assertNull(event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); + assertEquals("realVariant", event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); + } + + @Test + void testErrorEvaluation() { + EvaluationContext evaluationContext = mock(EvaluationContext.class); + when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); + + Metadata providerMetadata = mock(Metadata.class); + when(providerMetadata.getName()).thenReturn("realProviderName"); + + HookContext hookContext = HookContext.builder() + .flagKey("realFlag") + .type(FlagValueType.STRING) + .defaultValue("realDefault") + .ctx(evaluationContext) + .clientMetadata(mock(ClientMetadata.class)) + .providerMetadata(providerMetadata) + .build(); + + FlagEvaluationDetails providerEvaluation = FlagEvaluationDetails.builder() + .flagMetadata(ImmutableMetadata.builder() + .addString("contextId", "realContextId") + .addString("flagSetId", "realFlagSetId") + .addString("version", "realVersion") + .build()) + .reason(Reason.ERROR.name()) + .errorMessage("realErrorMessage") + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); + + assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); + assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); + assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); + assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); + assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); + assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); + assertEquals(ErrorCode.GENERAL, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); + assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG)); + assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); + } + + @Test + void testErrorCodeEvaluation() { + EvaluationContext evaluationContext = mock(EvaluationContext.class); + when(evaluationContext.getTargetingKey()).thenReturn("realTargetingKey"); + + Metadata providerMetadata = mock(Metadata.class); + when(providerMetadata.getName()).thenReturn("realProviderName"); + + HookContext hookContext = HookContext.builder() + .flagKey("realFlag") + .type(FlagValueType.STRING) + .defaultValue("realDefault") + .ctx(evaluationContext) + .clientMetadata(mock(ClientMetadata.class)) + .providerMetadata(providerMetadata) + .build(); + + FlagEvaluationDetails providerEvaluation = FlagEvaluationDetails.builder() + .flagMetadata(ImmutableMetadata.builder() + .addString("contextId", "realContextId") + .addString("flagSetId", "realFlagSetId") + .addString("version", "realVersion") + .build()) + .reason(Reason.ERROR.name()) + .errorMessage("realErrorMessage") + .errorCode(ErrorCode.INVALID_CONTEXT) + .build(); + + EvaluationEvent event = Telemetry.createEvaluationEvent(hookContext, providerEvaluation); + + assertEquals("realFlag", event.getAttributes().get(Telemetry.TELEMETRY_KEY)); + assertEquals("realProviderName", event.getAttributes().get(Telemetry.TELEMETRY_PROVIDER)); + assertEquals("error", event.getAttributes().get(Telemetry.TELEMETRY_REASON)); + assertEquals("realContextId", event.getAttributes().get(Telemetry.TELEMETRY_CONTEXT_ID)); + assertEquals("realFlagSetId", event.getAttributes().get(Telemetry.TELEMETRY_FLAG_SET_ID)); + assertEquals("realVersion", event.getAttributes().get(Telemetry.TELEMETRY_VERSION)); + assertEquals(ErrorCode.INVALID_CONTEXT, event.getAttributes().get(Telemetry.TELEMETRY_ERROR_CODE)); + assertEquals("realErrorMessage", event.getAttributes().get(Telemetry.TELEMETRY_ERROR_MSG)); + assertNull(event.getAttributes().get(Telemetry.TELEMETRY_VARIANT)); + } +} diff --git a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java index a8f6e30f3..ba3543745 100644 --- a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java @@ -15,7 +15,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import dev.openfeature.sdk.fixtures.ProviderFixture; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import java.util.HashMap; import java.util.Map; import lombok.SneakyThrows; @@ -29,7 +28,7 @@ class TrackingSpecTest { @BeforeEach void getApiInstance() { - api = OpenFeatureAPI.getInstance(); + api = new OpenFeatureAPI(); client = api.getClient(); } @@ -116,7 +115,7 @@ void contextsGetMerged() { client.setEvaluationContext(clCtx); FeatureProvider provider = ProviderFixture.createMockedProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); client.track("event", new MutableContext().add("my-key", "final"), new MutableTrackingEventDetails(0.0f)); @@ -170,8 +169,7 @@ void eventDetails() { .add("my-struct", new Value(new MutableTrackingEventDetails())); assertEquals(expectedMap, details.asMap()); - assertThatCode(() -> OpenFeatureAPI.getInstance() - .getClient() + assertThatCode(() -> api.getClient() .track("tracking-event-name", new ImmutableContext(), new MutableTrackingEventDetails())) .doesNotThrowAnyException(); @@ -188,8 +186,7 @@ void eventDetails() { ImmutableTrackingEventDetails immutableDetails = new ImmutableTrackingEventDetails(2, expectedMap); assertEquals(expectedImmutable, immutableDetails.asMap()); - assertThatCode(() -> OpenFeatureAPI.getInstance() - .getClient() + assertThatCode(() -> api.getClient() .track("tracking-event-name", new ImmutableContext(), new ImmutableTrackingEventDetails())) .doesNotThrowAnyException(); } diff --git a/src/test/java/dev/openfeature/sdk/ValueTest.java b/src/test/java/dev/openfeature/sdk/ValueTest.java index c25538508..697edb7be 100644 --- a/src/test/java/dev/openfeature/sdk/ValueTest.java +++ b/src/test/java/dev/openfeature/sdk/ValueTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -11,15 +12,15 @@ import java.util.List; import org.junit.jupiter.api.Test; -public class ValueTest { +class ValueTest { @Test - public void noArgShouldContainNull() { + void noArgShouldContainNull() { Value value = new Value(); assertTrue(value.isNull()); } @Test - public void objectArgShouldContainObject() { + void objectArgShouldContainObject() { try { // int is a special case, see intObjectArgShouldConvertToInt() List list = new ArrayList<>(); @@ -42,7 +43,7 @@ public void objectArgShouldContainObject() { } @Test - public void intObjectArgShouldConvertToInt() { + void intObjectArgShouldConvertToInt() { try { Object innerValue = 1; Value value = new Value(innerValue); @@ -53,7 +54,7 @@ public void intObjectArgShouldConvertToInt() { } @Test - public void invalidObjectArgShouldThrow() { + void invalidObjectArgShouldThrow() { class Something {} @@ -63,7 +64,7 @@ class Something {} } @Test - public void boolArgShouldContainBool() { + void boolArgShouldContainBool() { boolean innerValue = true; Value value = new Value(innerValue); assertTrue(value.isBoolean()); @@ -71,7 +72,7 @@ public void boolArgShouldContainBool() { } @Test - public void numericArgShouldReturnDoubleOrInt() { + void numericArgShouldReturnDoubleOrInt() { double innerDoubleValue = 1.75; Value doubleValue = new Value(innerDoubleValue); assertTrue(doubleValue.isNumber()); @@ -86,7 +87,7 @@ public void numericArgShouldReturnDoubleOrInt() { } @Test - public void stringArgShouldContainString() { + void stringArgShouldContainString() { String innerValue = "hi!"; Value value = new Value(innerValue); assertTrue(value.isString()); @@ -94,7 +95,7 @@ public void stringArgShouldContainString() { } @Test - public void dateShouldContainDate() { + void dateShouldContainDate() { Instant innerValue = Instant.now(); Value value = new Value(innerValue); assertTrue(value.isInstant()); @@ -102,7 +103,7 @@ public void dateShouldContainDate() { } @Test - public void structureShouldContainStructure() { + void structureShouldContainStructure() { String INNER_KEY = "key"; String INNER_VALUE = "val"; MutableStructure innerValue = new MutableStructure().add(INNER_KEY, INNER_VALUE); @@ -112,7 +113,7 @@ public void structureShouldContainStructure() { } @Test - public void listArgShouldContainList() { + void listArgShouldContainList() { String ITEM_VALUE = "val"; List innerValue = new ArrayList(); innerValue.add(new Value(ITEM_VALUE)); @@ -122,7 +123,7 @@ public void listArgShouldContainList() { } @Test - public void listMustBeOfValues() { + void listMustBeOfValues() { String item = "item"; List list = new ArrayList<>(); list.add(item); @@ -135,7 +136,7 @@ public void listMustBeOfValues() { } @Test - public void emptyListAllowed() { + void emptyListAllowed() { List list = new ArrayList<>(); try { Value value = new Value((Object) list); @@ -148,7 +149,7 @@ public void emptyListAllowed() { } @Test - public void valueConstructorValidateListInternals() { + void valueConstructorValidateListInternals() { List list = new ArrayList<>(); list.add(new Value("item")); list.add("item"); @@ -157,8 +158,22 @@ public void valueConstructorValidateListInternals() { } @Test - public void noOpFinalize() { + void noOpFinalize() { Value val = new Value(); assertDoesNotThrow(val::finalize); // does nothing, but we want to defined in and make it final. } + + @Test + void equalValuesShouldBeEqual() { + Value val1 = new Value(12312312); + Value val2 = new Value(12312312); + assertEquals(val1, val2); + } + + @Test + void unequalValuesShouldNotBeEqual() { + Value val1 = new Value("a"); + Value val2 = new Value("b"); + assertNotEquals(val1, val2); + } } diff --git a/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java b/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java new file mode 100644 index 000000000..8bf8b2888 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java @@ -0,0 +1,27 @@ +package dev.openfeature.sdk.arch; + +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses(packages = "dev.openfeature.sdk") +public class ArchitectureTest { + + @ArchTest + public static final ArchRule avoidGetInstances = noClasses() + .that() + .resideOutsideOfPackages("..benchmark", "..e2e.*") + .and() + .haveSimpleNameNotEndingWith("SingeltonTest") + .should() + .callMethodWhere(describe( + "Avoid Internal usage of OpenFeatureAPI.GetInstances", + // Target method may not reside in class annotated with BusinessException + methodCall -> + methodCall.getTarget().getOwner().getFullName().equals("dev.openfeature.sdk.OpenFeatureAPI") + // And target method may not have the static modifier + && methodCall.getTarget().getName().equals("getInstance"))); +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/ContextStoringProvider.java b/src/test/java/dev/openfeature/sdk/e2e/ContextStoringProvider.java new file mode 100644 index 000000000..e06e862a5 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/ContextStoringProvider.java @@ -0,0 +1,48 @@ +package dev.openfeature.sdk.e2e; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import lombok.Getter; + +@Getter +public class ContextStoringProvider implements FeatureProvider { + private EvaluationContext evaluationContext; + + @Override + public Metadata getMetadata() { + return () -> getClass().getSimpleName(); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + this.evaluationContext = ctx; + return null; + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + this.evaluationContext = ctx; + return null; + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + this.evaluationContext = ctx; + return null; + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + this.evaluationContext = ctx; + return null; + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + this.evaluationContext = ctx; + return null; + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java index 8a3381412..b7c834312 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java +++ b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java @@ -1,16 +1,18 @@ package dev.openfeature.sdk.e2e; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.OBJECT_FACTORY_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; -import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.SelectDirectories; import org.junit.platform.suite.api.Suite; @Suite @IncludeEngines("cucumber") -@SelectClasspathResource("features/evaluation.feature") +@SelectDirectories("spec/specification/assets/gherkin") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") -@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.evaluation") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.steps") +@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") public class EvaluationTest {} diff --git a/src/test/java/dev/openfeature/sdk/e2e/Flag.java b/src/test/java/dev/openfeature/sdk/e2e/Flag.java new file mode 100644 index 000000000..2c4ffdb57 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/Flag.java @@ -0,0 +1,13 @@ +package dev.openfeature.sdk.e2e; + +public class Flag { + public String name; + public Object defaultValue; + public String type; + + public Flag(String type, String name, Object defaultValue) { + this.name = name; + this.defaultValue = defaultValue; + this.type = type; + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/MockHook.java b/src/test/java/dev/openfeature/sdk/e2e/MockHook.java new file mode 100644 index 000000000..ac107cfd6 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/MockHook.java @@ -0,0 +1,50 @@ +package dev.openfeature.sdk.e2e; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import lombok.Getter; + +public class MockHook implements Hook { + @Getter + private boolean beforeCalled; + + @Getter + private boolean afterCalled; + + @Getter + private boolean errorCalled; + + @Getter + private boolean finallyAfterCalled; + + @Getter + private final Map evaluationDetails = new HashMap<>(); + + @Override + public Optional before(HookContext ctx, Map hints) { + beforeCalled = true; + return Optional.of(ctx.getCtx()); + } + + @Override + public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { + afterCalled = true; + evaluationDetails.put("after", details); + } + + @Override + public void error(HookContext ctx, Exception error, Map hints) { + errorCalled = true; + } + + @Override + public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) { + finallyAfterCalled = true; + evaluationDetails.put("finally", details); + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/State.java b/src/test/java/dev/openfeature/sdk/e2e/State.java new file mode 100644 index 000000000..68c708b4a --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/State.java @@ -0,0 +1,19 @@ +package dev.openfeature.sdk.e2e; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.MutableContext; +import java.util.List; + +public class State { + public Client client; + public Flag flag; + public MutableContext context = new MutableContext(); + public FlagEvaluationDetails evaluation; + public MockHook hook; + public FeatureProvider provider; + public EvaluationContext invocationContext; + public List levels; +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/Utils.java b/src/test/java/dev/openfeature/sdk/e2e/Utils.java new file mode 100644 index 000000000..902ee11d0 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/Utils.java @@ -0,0 +1,28 @@ +package dev.openfeature.sdk.e2e; + +import java.util.Objects; + +public final class Utils { + + private Utils() {} + + public static Object convert(String value, String type) { + if (Objects.equals(value, "null")) { + return null; + } + switch (type.toLowerCase()) { + case "boolean": + return Boolean.parseBoolean(value); + case "string": + return value; + case "integer": + return Integer.parseInt(value); + case "float": + case "double": + return Double.parseDouble(value); + case "long": + return Long.parseLong(value); + } + throw new RuntimeException("Unknown config type: " + type); + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java b/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java new file mode 100644 index 000000000..ccb78e72a --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java @@ -0,0 +1,104 @@ +package dev.openfeature.sdk.e2e.steps; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.e2e.ContextStoringProvider; +import dev.openfeature.sdk.e2e.State; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ContextSteps { + private final State state; + + public ContextSteps(State state) { + this.state = state; + } + + @Given("a stable provider with retrievable context is registered") + public void setup() { + ContextStoringProvider provider = new ContextStoringProvider(); + state.provider = provider; + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + state.client = OpenFeatureAPI.getInstance().getClient(); + OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + } + + @When("A context entry with key {string} and value {string} is added to the {string} level") + public void aContextWithKeyAndValueIsAddedToTheLevel(String contextKey, String contextValue, String level) { + addContextEntry(contextKey, contextValue, level); + } + + private void addContextEntry(String contextKey, String contextValue, String level) { + Map data = new HashMap<>(); + data.put(contextKey, new Value(contextValue)); + EvaluationContext context = new ImmutableContext(data); + if ("API".equals(level)) { + OpenFeatureAPI.getInstance().setEvaluationContext(context); + } else if ("Transaction".equals(level)) { + OpenFeatureAPI.getInstance().setTransactionContext(context); + } else if ("Client".equals(level)) { + state.client.setEvaluationContext(context); + } else if ("Invocation".equals(level)) { + state.invocationContext = context; + } else if ("Before Hooks".equals(level)) { + state.client.addHooks(new Hook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.of(context); + } + }); + } else { + throw new IllegalArgumentException("Unknown level: " + level); + } + } + + @When("Some flag was evaluated") + public void someFlagWasEvaluated() { + state.evaluation = state.client.getStringDetails("unused", "unused", state.invocationContext); + } + + @Then("The merged context contains an entry with key {string} and value {string}") + public void theMergedContextContainsAnEntryWithKeyAndValue(String contextKey, String contextValue) { + assertInstanceOf( + ContextStoringProvider.class, + state.provider, + "In order to use this step, you need to set a ContextStoringProvider"); + EvaluationContext ctx = ((ContextStoringProvider) state.provider).getEvaluationContext(); + assertNotNull(ctx); + assertNotNull(ctx.getValue(contextKey)); + assertNotNull(ctx.getValue(contextKey).asString()); + assertEquals(contextValue, ctx.getValue(contextKey).asString()); + } + + @Given("A table with levels of increasing precedence") + public void aTableWithLevelsOfIncreasingPrecedence(DataTable levelsTable) { + state.levels = levelsTable.asList(); + } + + @And( + "Context entries for each level from API level down to the {string} level, with key {string} and value {string}") + public void contextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue( + String maxLevel, String key, String value) { + for (String level : state.levels) { + addContextEntry(key, value, level); + if (level.equals(maxLevel)) { + return; + } + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java new file mode 100644 index 000000000..390e067f3 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java @@ -0,0 +1,104 @@ +package dev.openfeature.sdk.e2e.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.e2e.Flag; +import dev.openfeature.sdk.e2e.State; +import dev.openfeature.sdk.e2e.Utils; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; + +public class FlagStepDefinitions { + private final State state; + + public FlagStepDefinitions(State state) { + this.state = state; + } + + @Given("a {}-flag with key {string} and a default value {string}") + public void givenAFlag(String type, String name, String defaultValue) { + state.flag = new Flag(type, name, Utils.convert(defaultValue, type)); + } + + @When("the flag was evaluated with details") + public void the_flag_was_evaluated_with_details() { + FlagEvaluationDetails details; + switch (state.flag.type.toLowerCase()) { + case "string": + details = + state.client.getStringDetails(state.flag.name, (String) state.flag.defaultValue, state.context); + break; + case "boolean": + details = state.client.getBooleanDetails( + state.flag.name, (Boolean) state.flag.defaultValue, state.context); + break; + case "float": + details = + state.client.getDoubleDetails(state.flag.name, (Double) state.flag.defaultValue, state.context); + break; + case "integer": + details = state.client.getIntegerDetails( + state.flag.name, (Integer) state.flag.defaultValue, state.context); + break; + case "object": + details = + state.client.getObjectDetails(state.flag.name, (Value) state.flag.defaultValue, state.context); + break; + default: + throw new AssertionError(); + } + state.evaluation = details; + } + + @Then("the resolved details value should be {string}") + public void the_resolved_details_value_should_be(String value) { + assertThat(state.evaluation.getValue()).isEqualTo(Utils.convert(value, state.flag.type)); + } + + @Then("the reason should be {string}") + public void the_reason_should_be(String reason) { + assertThat(state.evaluation.getReason()).isEqualTo(reason); + } + + @Then("the variant should be {string}") + public void the_variant_should_be(String variant) { + assertThat(state.evaluation.getVariant()).isEqualTo(variant); + } + + @Then("the resolved metadata value \"{}\" with type \"{}\" should be \"{}\"") + public void theResolvedMetadataValueShouldBe(String key, String type, String value) + throws NoSuchFieldException, IllegalAccessException { + Field f = state.evaluation.getFlagMetadata().getClass().getDeclaredField("metadata"); + f.setAccessible(true); + HashMap metadata = (HashMap) f.get(state.evaluation.getFlagMetadata()); + assertThat(metadata).containsEntry(key, Utils.convert(value, type)); + } + + @Then("the resolved metadata is empty") + public void theResolvedMetadataIsEmpty() { + assertThat(state.evaluation.getFlagMetadata().isEmpty()).isTrue(); + } + + @Then("the resolved metadata should contain") + public void theResolvedMetadataShouldContain(DataTable dataTable) { + ImmutableMetadata evaluationMetadata = state.evaluation.getFlagMetadata(); + List> asLists = dataTable.asLists(); + for (int i = 1; i < asLists.size(); i++) { // skip the header of the table + List line = asLists.get(i); + String key = line.get(0); + String metadataType = line.get(1); + Object value = Utils.convert(line.get(2), metadataType); + + assertThat(value).isNotNull(); + assertThat(evaluationMetadata.getValue(key, value.getClass())).isEqualTo(value); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/HookSteps.java b/src/test/java/dev/openfeature/sdk/e2e/steps/HookSteps.java new file mode 100644 index 000000000..1e6a9172f --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/HookSteps.java @@ -0,0 +1,84 @@ +package dev.openfeature.sdk.e2e.steps; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.e2e.MockHook; +import dev.openfeature.sdk.e2e.State; +import dev.openfeature.sdk.e2e.Utils; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import java.util.List; +import java.util.Map; + +public class HookSteps { + private final State state; + + public HookSteps(State state) { + this.state = state; + } + + @Given("a client with added hook") + public void aClientWithAddedHook() { + MockHook hook = new MockHook(); + state.hook = hook; + state.client.addHooks(hook); + } + + @Then("the {string} hook should have been executed") + public void theHookShouldHaveBeenExecuted(String hookName) { + assertHookCalled(hookName); + } + + public void assertHookCalled(String hookName) { + if ("before".equals(hookName)) { + assertTrue(state.hook.isBeforeCalled()); + } else if ("after".equals(hookName)) { + assertTrue(state.hook.isAfterCalled()); + } else if ("error".equals(hookName)) { + assertTrue(state.hook.isErrorCalled()); + } else if ("finally".equals(hookName)) { + assertTrue(state.hook.isFinallyAfterCalled()); + } else { + throw new IllegalArgumentException(hookName + " is not a valid hook name"); + } + } + + @And("the {string} hooks should be called with evaluation details") + public void theHooksShouldBeCalledWithEvaluationDetails(String hookNames, DataTable data) { + for (String hookName : hookNames.split(", ")) { + assertHookCalled(hookName); + FlagEvaluationDetails evaluationDetails = + state.hook.getEvaluationDetails().get(hookName); + assertNotNull(evaluationDetails); + List> dataEntries = data.asMaps(); + for (Map line : dataEntries) { + String key = line.get("key"); + Object expected = Utils.convert(line.get("value"), line.get("data_type")); + Object actual; + if ("flag_key".equals(key)) { + actual = evaluationDetails.getFlagKey(); + } else if ("value".equals(key)) { + actual = evaluationDetails.getValue(); + } else if ("variant".equals(key)) { + actual = evaluationDetails.getVariant(); + } else if ("reason".equals(key)) { + actual = evaluationDetails.getReason(); + } else if ("error_code".equals(key)) { + actual = evaluationDetails.getErrorCode(); + if (actual != null) { + actual = actual.toString(); + } + } else { + throw new IllegalArgumentException(key + " is not a valid key"); + } + + assertEquals(expected, actual); + } + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java b/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java new file mode 100644 index 000000000..82cdb2e79 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java @@ -0,0 +1,26 @@ +package dev.openfeature.sdk.e2e.steps; + +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; + +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.e2e.State; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import io.cucumber.java.en.Given; +import java.util.Map; + +public class ProviderSteps { + private final State state; + + public ProviderSteps(State state) { + this.state = state; + } + + @Given("a stable provider") + public void aStableProvider() { + Map> flags = buildFlags(); + InMemoryProvider provider = new InMemoryProvider(flags); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + state.client = OpenFeatureAPI.getInstance().getClient(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java similarity index 94% rename from src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java rename to src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java index c1e56429d..924c9d59e 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java @@ -1,8 +1,7 @@ -package dev.openfeature.sdk.e2e.evaluation; +package dev.openfeature.sdk.e2e.steps; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.EvaluationContext; @@ -289,7 +288,7 @@ public void then_the_default_string_value_should_be_returned() { @Then("the reason should indicate an error and the error code should indicate a missing flag with {string}") public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) { assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason()); - assertTrue(notFoundDetails.getErrorCode().name().equals(errorCode)); + assertEquals(errorCode, notFoundDetails.getErrorCode().name()); } // type mismatch @@ -309,6 +308,23 @@ public void then_the_default_integer_value_should_be_returned() { @Then("the reason should indicate an error and the error code should indicate a type mismatch with {string}") public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) { assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason()); - assertTrue(typeErrorDetails.getErrorCode().name().equals(errorCode)); + assertEquals(errorCode, typeErrorDetails.getErrorCode().name()); + } + + @SuppressWarnings("java:S2925") + @When("sleep for {int} milliseconds") + public void sleepForMilliseconds(int millis) { + long startTime = System.currentTimeMillis(); + long endTime = startTime + millis; + long now; + while ((now = System.currentTimeMillis()) < endTime) { + long remainingTime = endTime - now; + try { + //noinspection BusyWait + Thread.sleep(remainingTime); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } } } diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index 86782b397..970495940 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -3,21 +3,28 @@ import static dev.openfeature.sdk.Structure.mapToStructure; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableMap; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.OpenFeatureAPITestUtil; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import dev.openfeature.sdk.exceptions.TypeMismatchError; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; @@ -25,18 +32,21 @@ class InMemoryProviderTest { - private static Client client; + private Client client; - private static InMemoryProvider provider; + private InMemoryProvider provider; + private OpenFeatureAPI api; @SneakyThrows @BeforeEach void beforeEach() { + final var configChangedEventCounter = new AtomicInteger(); Map> flags = buildFlags(); provider = spy(new InMemoryProvider(flags)); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> {}); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - client = OpenFeatureAPI.getInstance().getClient(); + api = OpenFeatureAPITestUtil.createAPI(); + api.onProviderConfigurationChanged(eventDetails -> configChangedEventCounter.incrementAndGet()); + api.setProviderAndWait(provider); + client = api.getClient(); provider.updateFlags(flags); provider.updateFlag( "addedFlag", @@ -45,6 +55,11 @@ void beforeEach() { .variant("off", false) .defaultVariant("on") .build()); + + // wait for the two config changed events to be fired, otherwise they could mess with our tests + while (configChangedEventCounter.get() < 2) { + Thread.sleep(1); + } } @Test @@ -107,8 +122,8 @@ void emitChangedFlagsOnlyIfThereAreChangedFlags() { Consumer handler = mock(Consumer.class); Map> flags = buildFlags(); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + api.onProviderConfigurationChanged(handler); + api.setProviderAndWait(provider); provider.updateFlags(flags); diff --git a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java b/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java deleted file mode 100644 index c9ad77d89..000000000 --- a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import static org.awaitility.Awaitility.await; - -import dev.openfeature.sdk.FeatureProvider; -import dev.openfeature.sdk.OpenFeatureAPI; -import java.time.Duration; -import java.util.function.Function; -import lombok.experimental.UtilityClass; - -// todo check the need of this utility class as we now have setProviderAndWait capability -@UtilityClass -public class FeatureProviderTestUtils { - - public static void setFeatureProvider(FeatureProvider provider) { - OpenFeatureAPI.getInstance().setProvider(provider); - waitForProviderInitializationComplete(OpenFeatureAPI::getProvider, provider); - } - - private static void waitForProviderInitializationComplete( - Function extractor, FeatureProvider provider) { - await().pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) - .until(() -> extractor.apply(OpenFeatureAPI.getInstance()).equals(provider)); - } - - public static void setFeatureProvider(String domain, FeatureProvider provider) { - OpenFeatureAPI.getInstance().setProvider(domain, provider); - waitForProviderInitializationComplete(api -> api.getProvider(domain), provider); - } -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java index 157b07175..c1767ff6f 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -3,6 +3,7 @@ import static dev.openfeature.sdk.Structure.mapToStructure; import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.providers.memory.Flag; import java.util.HashMap; @@ -22,9 +23,11 @@ public class TestFlagsUtils { public static final String OBJECT_FLAG_KEY = "object-flag"; public static final String CONTEXT_AWARE_FLAG_KEY = "context-aware"; public static final String WRONG_FLAG_KEY = "wrong-flag"; + public static final String METADATA_FLAG_KEY = "metadata-flag"; /** * Building flags for testing purposes. + * * @return map of flags */ public static Map> buildFlags() { @@ -90,6 +93,19 @@ public static Map> buildFlags() { .variant("two", "dos") .defaultVariant("one") .build()); + flags.put( + METADATA_FLAG_KEY, + Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .flagMetadata(ImmutableMetadata.builder() + .addString("string", "1.0.2") + .addInteger("integer", 2) + .addBoolean("boolean", true) + .addDouble("float", 0.1d) + .build()) + .build()); return flags; } } diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestStackedEmitCallsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestStackedEmitCallsProvider.java new file mode 100644 index 000000000..d1bf65c57 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/TestStackedEmitCallsProvider.java @@ -0,0 +1,103 @@ +package dev.openfeature.sdk.testutils; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.ProviderEvent; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.Value; +import java.util.function.Consumer; + +public class TestStackedEmitCallsProvider extends EventProvider { + private final NestedBlockingEmitter nestedBlockingEmitter = new NestedBlockingEmitter(this::onProviderEvent); + + @Override + public Metadata getMetadata() { + return () -> getClass().getSimpleName(); + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + synchronized (nestedBlockingEmitter) { + nestedBlockingEmitter.init(); + while (!nestedBlockingEmitter.isReady()) { + try { + nestedBlockingEmitter.wait(); + } catch (InterruptedException e) { + } + } + } + } + + private void onProviderEvent(ProviderEvent providerEvent) { + synchronized (nestedBlockingEmitter) { + if (providerEvent == ProviderEvent.PROVIDER_READY) { + nestedBlockingEmitter.setReady(); + /* + * This line deadlocked in the original implementation without the emitterExecutor see + * https://github.com/open-feature/java-sdk/issues/1299 + */ + emitProviderReady(ProviderEventDetails.builder().build()); + } + } + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'"); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'"); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'"); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'"); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'"); + } + + static class NestedBlockingEmitter { + + private final Consumer emitProviderEvent; + private volatile boolean isReady; + + public NestedBlockingEmitter(Consumer emitProviderEvent) { + this.emitProviderEvent = emitProviderEvent; + } + + public void init() { + // run init outside monitored thread + new Thread(() -> { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + emitProviderEvent.accept(ProviderEvent.PROVIDER_READY); + }) + .start(); + } + + public boolean isReady() { + return isReady; + } + + public synchronized void setReady() { + isReady = true; + this.notifyAll(); + } + } +} diff --git a/version.txt b/version.txt index 850e74240..ace44233b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.14.0 +1.15.1