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..a3c228ef4 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@069817c298f23fab00a8f29a2e556a5eac0f6390 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 338a0985c..8c978f625 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -9,35 +9,39 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + with: + fetch-depth: 0 + submodules: recursive + - name: Set up JDK 17 + uses: actions/setup-java@46c56d6f92c88cf540acf95a12a4a41197499222 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven - server-id: ossrh - server-username: ${{ secrets.OSSRH_USERNAME }} - server-password: ${{ secrets.OSSRH_PASSWORD }} + server-id: central + server-username: ${{ secrets.CENTRAL_USERNAME }} + server-password: ${{ secrets.CENTRAL_PASSWORD }} - name: Cache local Maven repository - uses: actions/cache@36f1e144e1c8edb0a652766b484448563d8baf46 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 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 +53,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.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -60,11 +64,11 @@ jobs: # Add -SNAPSHOT before deploy - name: Add SNAPSHOT run: mvn versions:set -DnewVersion='${project.version}-SNAPSHOT' - + - name: Deploy 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 }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index d7ade6207..7e815801d 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -7,36 +7,49 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + with: + fetch-depth: 0 + submodules: recursive - - name: Set up JDK 8 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + - name: Set up JDK ${{ matrix.build.java }} + uses: actions/setup-java@46c56d6f92c88cf540acf95a12a4a41197499222 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@ce729e4d353d580e6cacd6a8cf2921b72e5e310a with: languages: java - name: Cache local Maven repository - uses: actions/cache@36f1e144e1c8edb0a652766b484448563d8baf46 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 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.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -45,4 +58,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/analyze@ce729e4d353d580e6cacd6a8cf2921b72e5e310a diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7342889db..841441307 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,46 +12,56 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + with: + fetch-depth: 0 + submodules: recursive + + - name: Set up JDK 17 + uses: actions/setup-java@46c56d6f92c88cf540acf95a12a4a41197499222 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven - server-id: ossrh - server-username: ${{ secrets.OSSRH_USERNAME }} - server-password: ${{ secrets.OSSRH_PASSWORD }} + server-id: central + server-username: ${{ secrets.CENTRAL_USERNAME }} + server-password: ${{ secrets.CENTRAL_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 + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 8e6d4ed29..c2de02806 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/init@ce729e4d353d580e6cacd6a8cf2921b72e5e310a with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/autobuild@ce729e4d353d580e6cacd6a8cf2921b72e5e310a - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0701025a8b1600e416be4f3bb5a830b1aa6af01e + uses: github/codeql-action/analyze@ce729e4d353d580e6cacd6a8cf2921b72e5e310a diff --git a/.gitignore b/.gitignore index a7575d545..dfc5642e5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ target # vscode stuff - we may want to use a more specific pattern later if we'd like to suggest editor configurations .vscode/ +.cursor # used for spec compliance tooling java-report.json diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb70b..44f3cf2c1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,2 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# 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.11/apache-maven-3.9.11-bin.zip diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e2d18dc18..f386789e1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.14.0"} \ No newline at end of file +{".":"1.18.2"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7000d46..cbb4c6135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,448 @@ # Changelog +## [1.18.2](https://github.com/open-feature/java-sdk/compare/v1.18.1...v1.18.2) (2025-10-06) + + +### ๐Ÿ› Bug Fixes + +* deployment failure because no tests were run by VMLens ([#1634](https://github.com/open-feature/java-sdk/issues/1634)) ([f6cb985](https://github.com/open-feature/java-sdk/commit/f6cb98556be3dacdb4e8d7770014dc2e7df65b6b)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.29.0 ([#1624](https://github.com/open-feature/java-sdk/issues/1624)) ([d0f9647](https://github.com/open-feature/java-sdk/commit/d0f9647fd09a8602ee47aae7778dbf7534c3fbd5)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.30.0 ([#1649](https://github.com/open-feature/java-sdk/issues/1649)) ([07bda4a](https://github.com/open-feature/java-sdk/commit/07bda4a99127e2c35d1e5323516c59f5beed20f8)) +* **deps:** update dependency org.junit:junit-bom to v5.14.0 ([#1646](https://github.com/open-feature/java-sdk/issues/1646)) ([2da33d6](https://github.com/open-feature/java-sdk/commit/2da33d60bc51ff9adadc3113d53106162e42f4b7)) +* **deps:** update dependency org.junit:junit-bom to v6 ([#1647](https://github.com/open-feature/java-sdk/issues/1647)) ([8893bf3](https://github.com/open-feature/java-sdk/commit/8893bf3817053e416d0d8656890441095357411b)) +* **deps:** update dependency org.projectlombok:lombok to v1.18.42 ([#1616](https://github.com/open-feature/java-sdk/issues/1616)) ([6dcd21f](https://github.com/open-feature/java-sdk/commit/6dcd21f559477c1b473cd30e459ca7241c4f7210)) +* for [#1611](https://github.com/open-feature/java-sdk/issues/1611) ([f6cb985](https://github.com/open-feature/java-sdk/commit/f6cb98556be3dacdb4e8d7770014dc2e7df65b6b)) +* improve vmlens handling ([#1628](https://github.com/open-feature/java-sdk/issues/1628)) ([fb3144a](https://github.com/open-feature/java-sdk/commit/fb3144a0b5e2f02e8af33d746fb6c426724adede)) + + +### โœจ New Features + +* add hook data support ([#1620](https://github.com/open-feature/java-sdk/issues/1620)) ([52c7f99](https://github.com/open-feature/java-sdk/commit/52c7f9906672320d08ad6e840dbaf4978d5fb6e2)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 0057852 ([#1631](https://github.com/open-feature/java-sdk/issues/1631)) ([c1944d2](https://github.com/open-feature/java-sdk/commit/c1944d2b1c1a85efb3b4f76a5836dfc0e1c9124b)) +* **deps:** update amannn/action-semantic-pull-request digest to e49f57c ([#1648](https://github.com/open-feature/java-sdk/issues/1648)) ([badac4f](https://github.com/open-feature/java-sdk/commit/badac4f6aaf8fb1a71f4a50f906b4c74603f10ae)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v3 ([#1632](https://github.com/open-feature/java-sdk/issues/1632)) ([0596ada](https://github.com/open-feature/java-sdk/commit/0596adac7dde1d9b5bf0da3763799d965870e828)) +* **deps:** update dependency com.google.guava:guava to v33.5.0-jre ([#1615](https://github.com/open-feature/java-sdk/issues/1615)) ([3ef41f5](https://github.com/open-feature/java-sdk/commit/3ef41f5225b93ca9d4b975c265d4ec41dbd91717)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v11.1.0 ([#1640](https://github.com/open-feature/java-sdk/issues/1640)) ([b686932](https://github.com/open-feature/java-sdk/commit/b686932fa86138e02bddaab0be79d61f39946a02)) +* **deps:** update dependency com.vmlens:api to v1.2.15 ([#1636](https://github.com/open-feature/java-sdk/issues/1636)) ([9bffa0a](https://github.com/open-feature/java-sdk/commit/9bffa0a45aaea6fb5ecf7b489c02cd502fc2d93c)) +* **deps:** update dependency com.vmlens:api to v1.2.16 ([#1642](https://github.com/open-feature/java-sdk/issues/1642)) ([4c18cc4](https://github.com/open-feature/java-sdk/commit/4c18cc4ee502030935796e98272fd45b6c394c11)) +* **deps:** update dependency com.vmlens:vmlens-maven-plugin to v1.2.15 ([#1637](https://github.com/open-feature/java-sdk/issues/1637)) ([139c9b2](https://github.com/open-feature/java-sdk/commit/139c9b21b73717e52fde4970561009cbd27addaa)) +* **deps:** update dependency com.vmlens:vmlens-maven-plugin to v1.2.16 ([#1643](https://github.com/open-feature/java-sdk/issues/1643)) ([01ce26a](https://github.com/open-feature/java-sdk/commit/01ce26afc9b8a4bc098dc3406e65c483fd940d15)) +* **deps:** update dependency maven-wrapper to v3.3.4 ([#1638](https://github.com/open-feature/java-sdk/issues/1638)) ([69a87a8](https://github.com/open-feature/java-sdk/commit/69a87a81e8f4796c5d4a895b9379c0103765ba3d)) +* **deps:** update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.1 ([#1625](https://github.com/open-feature/java-sdk/issues/1625)) ([55c344a](https://github.com/open-feature/java-sdk/commit/55c344a8323fa94f4b890d5a119976e879bbb43c)) +* **deps:** update dependency org.apache.maven.plugins:maven-dependency-plugin to v3.9.0 ([#1651](https://github.com/open-feature/java-sdk/issues/1651)) ([bb42184](https://github.com/open-feature/java-sdk/commit/bb4218456e87391ae4ad96999e6cb49f78eab0aa)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.12.0 ([#1623](https://github.com/open-feature/java-sdk/issues/1623)) ([eeda099](https://github.com/open-feature/java-sdk/commit/eeda09980cc85a538f0c41fef10bf285b0829761)) +* **deps:** update dependency org.assertj:assertj-core to v3.27.5 ([#1619](https://github.com/open-feature/java-sdk/issues/1619)) ([d4beca7](https://github.com/open-feature/java-sdk/commit/d4beca71bb60359b88e739153ff96ffc11aa74ef)) +* **deps:** update dependency org.assertj:assertj-core to v3.27.6 ([#1627](https://github.com/open-feature/java-sdk/issues/1627)) ([14f550f](https://github.com/open-feature/java-sdk/commit/14f550fd4ff9223437cc36d6cc1b248125a68e3c)) +* **deps:** update dependency org.codehaus.mojo:exec-maven-plugin to v3.6.0 ([#1645](https://github.com/open-feature/java-sdk/issues/1645)) ([261ea5d](https://github.com/open-feature/java-sdk/commit/261ea5dfa6b73d6e538388315719b0f06f417927)) +* **deps:** update dependency org.codehaus.mojo:exec-maven-plugin to v3.6.1 ([#1653](https://github.com/open-feature/java-sdk/issues/1653)) ([71dfb08](https://github.com/open-feature/java-sdk/commit/71dfb08a096a03fb02e44be4ada796e99b053cf0)) +* **deps:** update dependency org.mockito:mockito-core to v5.20.0 ([#1622](https://github.com/open-feature/java-sdk/issues/1622)) ([857fb9d](https://github.com/open-feature/java-sdk/commit/857fb9d78c509ee9c1087c355eb0fdb4ffdb668e)) +* **deps:** update dependency org.sonatype.central:central-publishing-maven-plugin to v0.9.0 ([#1630](https://github.com/open-feature/java-sdk/issues/1630)) ([014e82a](https://github.com/open-feature/java-sdk/commit/014e82af10ae478596c65c80d2cf92fc843339ed)) +* **deps:** update github/codeql-action digest to 0337c4c ([#1621](https://github.com/open-feature/java-sdk/issues/1621)) ([6cf64d6](https://github.com/open-feature/java-sdk/commit/6cf64d6b0c0ae351a707d70257380f2bce81a82e)) +* **deps:** update github/codeql-action digest to 065c6cf ([#1652](https://github.com/open-feature/java-sdk/issues/1652)) ([fe44e51](https://github.com/open-feature/java-sdk/commit/fe44e519c9de5dbc00b25aeaa6f1bf31809688f0)) +* **deps:** update github/codeql-action digest to 12dda79 ([#1618](https://github.com/open-feature/java-sdk/issues/1618)) ([17d0e48](https://github.com/open-feature/java-sdk/commit/17d0e487f31864facc0e69ee76352cab82b6b6b7)) +* **deps:** update github/codeql-action digest to 21a7ba3 ([#1650](https://github.com/open-feature/java-sdk/issues/1650)) ([1de446d](https://github.com/open-feature/java-sdk/commit/1de446d38ee2abdc3149f8eff374c617c26462fb)) +* **deps:** update github/codeql-action digest to 36adfa7 ([#1641](https://github.com/open-feature/java-sdk/issues/1641)) ([2155cc9](https://github.com/open-feature/java-sdk/commit/2155cc94371a0147fafc3246e7d4e4b9d9b7a5a3)) +* **deps:** update github/codeql-action digest to 6a87ebe ([#1639](https://github.com/open-feature/java-sdk/issues/1639)) ([58b6575](https://github.com/open-feature/java-sdk/commit/58b6575410b75760d5402f3f927dc8a2e62e9654)) +* **deps:** update github/codeql-action digest to 80cb6b5 ([#1644](https://github.com/open-feature/java-sdk/issues/1644)) ([6b922a2](https://github.com/open-feature/java-sdk/commit/6b922a2a6107cff7f951dcaf9e1c09609aeade83)) +* **deps:** update github/codeql-action digest to 94a9b7a ([#1635](https://github.com/open-feature/java-sdk/issues/1635)) ([f9796e8](https://github.com/open-feature/java-sdk/commit/f9796e8e1623bdac0ec52b7102e11759d3e449d8)) +* **deps:** update github/codeql-action digest to e4b85ab ([#1626](https://github.com/open-feature/java-sdk/issues/1626)) ([99a997d](https://github.com/open-feature/java-sdk/commit/99a997dcc594e06662cc3509e9c8698611893567)) + +## [1.18.1](https://github.com/open-feature/java-sdk/compare/v1.18.0...v1.18.1) (2025-09-17) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency com.fasterxml.jackson:jackson-bom to v2.20.0 ([#1604](https://github.com/open-feature/java-sdk/issues/1604)) ([b693390](https://github.com/open-feature/java-sdk/commit/b69339067a75a524911dded9798a06e58d628bce)) +* **deps:** update dependency com.github.spotbugs:spotbugs to v4.9.6 ([#1609](https://github.com/open-feature/java-sdk/issues/1609)) ([45ff89f](https://github.com/open-feature/java-sdk/commit/45ff89f530e8c73636618676b9db46c61235df57)) +* revert hook data to resolve bytecode incompatibility ([#1613](https://github.com/open-feature/java-sdk/issues/1613)) ([9845601](https://github.com/open-feature/java-sdk/commit/984560196d4a34fb21c8946d1dc675cf6565e90f)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/setup-java digest to ead9eaa ([#1608](https://github.com/open-feature/java-sdk/issues/1608)) ([a40667e](https://github.com/open-feature/java-sdk/commit/a40667e9cb635f9ba31f6975ba72bc5932fca094)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.6.0 ([#1612](https://github.com/open-feature/java-sdk/issues/1612)) ([fa23e96](https://github.com/open-feature/java-sdk/commit/fa23e960ff9dc52b5c44b56e521485ca03e0e650)) +* **deps:** update dependency com.vmlens:vmlens-maven-plugin to v1.2.14 ([#1606](https://github.com/open-feature/java-sdk/issues/1606)) ([a92a367](https://github.com/open-feature/java-sdk/commit/a92a367fef71cc9ec26f6b1b6609fb8e6b2543bc)) +* **deps:** update dependency dev.cel:cel to v0.11.0 ([#1603](https://github.com/open-feature/java-sdk/issues/1603)) ([e792221](https://github.com/open-feature/java-sdk/commit/e7922212d8cd964a8a24dacc518ff31c77fcfae6)) +* **deps:** update github/codeql-action digest to 573acd9 ([#1600](https://github.com/open-feature/java-sdk/issues/1600)) ([6fb139f](https://github.com/open-feature/java-sdk/commit/6fb139f8425fb76368b1e1baa745aa182f8c13f7)) +* fix checkout ([#1614](https://github.com/open-feature/java-sdk/issues/1614)) ([fbf2a75](https://github.com/open-feature/java-sdk/commit/fbf2a752647817d3e25d4f8b67893ba5e8f89b7a)) +* relax coverage ([69c5a12](https://github.com/open-feature/java-sdk/commit/69c5a1244283d22af0119f534270726a8c520367)) + +## [1.18.0](https://github.com/open-feature/java-sdk/compare/v1.17.0...v1.18.0) (2025-09-16) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency com.github.spotbugs:spotbugs to v4.9.4 ([#1548](https://github.com/open-feature/java-sdk/issues/1548)) ([f2fef65](https://github.com/open-feature/java-sdk/commit/f2fef65eb580b1960ad15b2c46ebe40855551be3)) +* **deps:** update dependency com.github.spotbugs:spotbugs to v4.9.5 ([#1598](https://github.com/open-feature/java-sdk/issues/1598)) ([5474c73](https://github.com/open-feature/java-sdk/commit/5474c736f711ed06cbca2bd0d4d0cfd458a54d10)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.27.1 ([#1562](https://github.com/open-feature/java-sdk/issues/1562)) ([508bdac](https://github.com/open-feature/java-sdk/commit/508bdac4f075e2cd374dd1728919cfc1619d0097)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.27.2 ([#1566](https://github.com/open-feature/java-sdk/issues/1566)) ([8a9f251](https://github.com/open-feature/java-sdk/commit/8a9f25177f2d4bab1b5215f172bbb8ed1a5ad788)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.28.2 ([#1593](https://github.com/open-feature/java-sdk/issues/1593)) ([ca72b19](https://github.com/open-feature/java-sdk/commit/ca72b19d33985dfaae1cda009afd6795c5752259)) +* **deps:** update dependency org.projectlombok:lombok to v1.18.40 ([#1597](https://github.com/open-feature/java-sdk/issues/1597)) ([e181276](https://github.com/open-feature/java-sdk/commit/e1812767b684b21d417e31dadc8d031cd64cdab1)) +* javadoc error on-merge action ([#1599](https://github.com/open-feature/java-sdk/issues/1599)) ([c126bdb](https://github.com/open-feature/java-sdk/commit/c126bdb2d772a85417dbb77b92079a8a2107b8b6)) +* make builder visible for javadocs, move javadoc gen to codequality profile ([c126bdb](https://github.com/open-feature/java-sdk/commit/c126bdb2d772a85417dbb77b92079a8a2107b8b6)) + + +### โœจ New Features + +* add hook data ([#1587](https://github.com/open-feature/java-sdk/issues/1587)) ([1b08e3d](https://github.com/open-feature/java-sdk/commit/1b08e3db42635bbe79c61437db2359bd74a98348)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 0400d5f ([#1544](https://github.com/open-feature/java-sdk/issues/1544)) ([050379b](https://github.com/open-feature/java-sdk/commit/050379be110b7a980cf5cc02ddf23e00a7ee7202)) +* **deps:** update actions/cache digest to 358a730 ([#1541](https://github.com/open-feature/java-sdk/issues/1541)) ([6efc2ee](https://github.com/open-feature/java-sdk/commit/6efc2ee1e701daf38e3efc2f115dc93025a0e2e2)) +* **deps:** update actions/cache digest to 638ed79 ([#1549](https://github.com/open-feature/java-sdk/issues/1549)) ([7f487ee](https://github.com/open-feature/java-sdk/commit/7f487ee7ecd1c69a63d22eca140bc422a0afc963)) +* **deps:** update actions/checkout digest to 08c6903 ([#1550](https://github.com/open-feature/java-sdk/issues/1550)) ([527e3f8](https://github.com/open-feature/java-sdk/commit/527e3f836f664ecace1018de84502592ee75007c)) +* **deps:** update actions/checkout digest to ff7abcd ([#1554](https://github.com/open-feature/java-sdk/issues/1554)) ([35fe5b4](https://github.com/open-feature/java-sdk/commit/35fe5b41bc971f8bc41b82b8c0ef50d719071301)) +* **deps:** update actions/setup-java digest to 0913e9a ([#1572](https://github.com/open-feature/java-sdk/issues/1572)) ([58c82de](https://github.com/open-feature/java-sdk/commit/58c82de9656562f137feafb9e6eff20521695803)) +* **deps:** update actions/setup-java digest to a7ab372 ([#1589](https://github.com/open-feature/java-sdk/issues/1589)) ([a08ff4d](https://github.com/open-feature/java-sdk/commit/a08ff4d96ccf9236ad882892280b600a05409394)) +* **deps:** update actions/setup-java digest to dded088 ([#1574](https://github.com/open-feature/java-sdk/issues/1574)) ([d332224](https://github.com/open-feature/java-sdk/commit/d33222439e84435baeb0a49d33651208e659ad27)) +* **deps:** update amannn/action-semantic-pull-request digest to 24e6f01 ([#1568](https://github.com/open-feature/java-sdk/issues/1568)) ([c8d48e1](https://github.com/open-feature/java-sdk/commit/c8d48e1a739f02b25e7980dcd5652851756729f2)) +* **deps:** update amannn/action-semantic-pull-request digest to 677b895 ([#1570](https://github.com/open-feature/java-sdk/issues/1570)) ([fb6ab35](https://github.com/open-feature/java-sdk/commit/fb6ab353b403542a1f7e6ccafb07e25ae1d5e2be)) +* **deps:** update amannn/action-semantic-pull-request digest to a46a7c8 ([#1563](https://github.com/open-feature/java-sdk/issues/1563)) ([47af527](https://github.com/open-feature/java-sdk/commit/47af5279d6abd0080eae16d630bd202976f9a4b1)) +* **deps:** update amannn/action-semantic-pull-request digest to e7d011b ([#1577](https://github.com/open-feature/java-sdk/issues/1577)) ([532ad2f](https://github.com/open-feature/java-sdk/commit/532ad2f3d404753189a529cdc262ba6ef8d3d586)) +* **deps:** update amannn/action-semantic-pull-request digest to fdd4d3d ([#1553](https://github.com/open-feature/java-sdk/issues/1553)) ([8a6c796](https://github.com/open-feature/java-sdk/commit/8a6c79627a492eb61ed35ffa91035b0737598b64)) +* **deps:** update codecov/codecov-action action to v5.5.0 ([#1573](https://github.com/open-feature/java-sdk/issues/1573)) ([2c2b380](https://github.com/open-feature/java-sdk/commit/2c2b380e13d2be5a988b70212ea243efdb60999b)) +* **deps:** update codecov/codecov-action action to v5.5.1 ([#1591](https://github.com/open-feature/java-sdk/issues/1591)) ([e47913a](https://github.com/open-feature/java-sdk/commit/e47913a07ee064fe340db978250710a7cb17795d)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.4.0 ([#1576](https://github.com/open-feature/java-sdk/issues/1576)) ([894165d](https://github.com/open-feature/java-sdk/commit/894165ddd7216dc30c1ca4c9f43c0a4c21970f17)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.4.2 ([#1592](https://github.com/open-feature/java-sdk/issues/1592)) ([8b3f7f0](https://github.com/open-feature/java-sdk/commit/8b3f7f07f4aa555301d484544939fa5a0b4de746)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.5.0 ([#1601](https://github.com/open-feature/java-sdk/issues/1601)) ([a7964be](https://github.com/open-feature/java-sdk/commit/a7964beda575c46eddc15f44d21c23b5a52802c7)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v11 ([#1543](https://github.com/open-feature/java-sdk/issues/1543)) ([d2f85d5](https://github.com/open-feature/java-sdk/commit/d2f85d5a9e42c0e336546684b27a1e77edc4c620)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v11.0.1 ([#1585](https://github.com/open-feature/java-sdk/issues/1585)) ([8dd40fa](https://github.com/open-feature/java-sdk/commit/8dd40fabcae6a90ee7abe1b0a0d8f55f345e3331)) +* **deps:** update dependency maven-wrapper to v3.3.3 ([#1584](https://github.com/open-feature/java-sdk/issues/1584)) ([cba90dd](https://github.com/open-feature/java-sdk/commit/cba90dd227514464fd90bd605f73c358748c09e1)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.7 ([#1560](https://github.com/open-feature/java-sdk/issues/1560)) ([124c26f](https://github.com/open-feature/java-sdk/commit/124c26f6ea2d20cebb7520afcd94ab6b6c8c8e7b)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.7 ([#1561](https://github.com/open-feature/java-sdk/issues/1561)) ([84887bf](https://github.com/open-feature/java-sdk/commit/84887bfc86eb4905f52bce4641c35a8909f2c463)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.4 ([#1596](https://github.com/open-feature/java-sdk/issues/1596)) ([3606154](https://github.com/open-feature/java-sdk/commit/3606154aa7ded57f41c324559268335531606b6c)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.3 ([#1559](https://github.com/open-feature/java-sdk/issues/1559)) ([6fbc9d6](https://github.com/open-feature/java-sdk/commit/6fbc9d6cca5716b7477f84ff4093ccc6af06d4e6)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.4 ([#1595](https://github.com/open-feature/java-sdk/issues/1595)) ([a17bd3a](https://github.com/open-feature/java-sdk/commit/a17bd3a388927781b86e91064748a71670bb2a2a)) +* **deps:** update dependency org.assertj:assertj-core to v3.27.4 ([#1546](https://github.com/open-feature/java-sdk/issues/1546)) ([c4fe7d2](https://github.com/open-feature/java-sdk/commit/c4fe7d2e81510db4eaa13a4d0046827efecd8a79)) +* **deps:** update dependency org.mockito:mockito-core to v5.19.0 ([#1557](https://github.com/open-feature/java-sdk/issues/1557)) ([dff5412](https://github.com/open-feature/java-sdk/commit/dff54123ab38fd3ad59809e5f863b6ace17e4da4)) +* **deps:** update github/codeql-action digest to 02ab253 ([#1583](https://github.com/open-feature/java-sdk/issues/1583)) ([080fc6e](https://github.com/open-feature/java-sdk/commit/080fc6e9a60c15eb748891245258c9c5378e4248)) +* **deps:** update github/codeql-action digest to 0d33fd9 ([#1590](https://github.com/open-feature/java-sdk/issues/1590)) ([9e4881f](https://github.com/open-feature/java-sdk/commit/9e4881fdcb577a693cfe587b58dce5a74fb3caf4)) +* **deps:** update github/codeql-action digest to 2330521 ([#1558](https://github.com/open-feature/java-sdk/issues/1558)) ([f1165da](https://github.com/open-feature/java-sdk/commit/f1165da1b910efda6dbe486a21eb7985cccf11ae)) +* **deps:** update github/codeql-action digest to 4474150 ([#1547](https://github.com/open-feature/java-sdk/issues/1547)) ([09b3138](https://github.com/open-feature/java-sdk/commit/09b31383e3b7e693ec43d6016486e3ededf8c2de)) +* **deps:** update github/codeql-action digest to 5b49155 ([#1575](https://github.com/open-feature/java-sdk/issues/1575)) ([d9c1df0](https://github.com/open-feature/java-sdk/commit/d9c1df0c5ec247f64c37648128d56b7e83444ca9)) +* **deps:** update github/codeql-action digest to 6dee5bc ([#1569](https://github.com/open-feature/java-sdk/issues/1569)) ([907b75d](https://github.com/open-feature/java-sdk/commit/907b75d3481e71621249b0246e0ca67c42c9a890)) +* **deps:** update github/codeql-action digest to 6ec994e ([#1564](https://github.com/open-feature/java-sdk/issues/1564)) ([19c2a12](https://github.com/open-feature/java-sdk/commit/19c2a1272a34040f3ac2b603c2bcb645f3707a82)) +* **deps:** update github/codeql-action digest to 6fe50b2 ([#1545](https://github.com/open-feature/java-sdk/issues/1545)) ([866235e](https://github.com/open-feature/java-sdk/commit/866235e494c229b3d47df80146199db4204b2ece)) +* **deps:** update github/codeql-action digest to 777f917 ([#1556](https://github.com/open-feature/java-sdk/issues/1556)) ([03a8018](https://github.com/open-feature/java-sdk/commit/03a8018098bbced558e157b60e7ba3ff18527db6)) +* **deps:** update github/codeql-action digest to 7eb43b0 ([#1555](https://github.com/open-feature/java-sdk/issues/1555)) ([88ded5d](https://github.com/open-feature/java-sdk/commit/88ded5d9290b3c008a597cf55bb1e1f4c05e40ec)) +* **deps:** update github/codeql-action digest to a880e53 ([#1581](https://github.com/open-feature/java-sdk/issues/1581)) ([5efac69](https://github.com/open-feature/java-sdk/commit/5efac69dae28ff26227e5a9e83c6edbea9b9b6b5)) +* **deps:** update github/codeql-action digest to aa90e97 ([#1594](https://github.com/open-feature/java-sdk/issues/1594)) ([5df4317](https://github.com/open-feature/java-sdk/commit/5df4317caa97bdd055ec1f809d2b5e49642b5312)) +* **deps:** update github/codeql-action digest to b1228d0 ([#1540](https://github.com/open-feature/java-sdk/issues/1540)) ([bc58780](https://github.com/open-feature/java-sdk/commit/bc587809341d60eeb575b0d58d75b35972b92053)) +* **deps:** update github/codeql-action digest to bbfff2f ([#1538](https://github.com/open-feature/java-sdk/issues/1538)) ([6056877](https://github.com/open-feature/java-sdk/commit/60568776c471e7c01f8cee6b198fe6df70fc2ca5)) +* **deps:** update github/codeql-action digest to c6dcdfa ([#1551](https://github.com/open-feature/java-sdk/issues/1551)) ([ce2b6e8](https://github.com/open-feature/java-sdk/commit/ce2b6e88313ea500c2b71f0b4c06ad4a12522f3e)) +* **deps:** update github/codeql-action digest to db69a51 ([#1571](https://github.com/open-feature/java-sdk/issues/1571)) ([fc7ec65](https://github.com/open-feature/java-sdk/commit/fc7ec6511fd92c872e55e2b4f96f1dc6215ed029)) +* **deps:** update github/codeql-action digest to e2b6f0f ([#1542](https://github.com/open-feature/java-sdk/issues/1542)) ([7ccbb71](https://github.com/open-feature/java-sdk/commit/7ccbb714b305deb9fa0e31ace1bd7f044c8c57ed)) +* **deps:** update github/codeql-action digest to e96e340 ([#1565](https://github.com/open-feature/java-sdk/issues/1565)) ([ea23114](https://github.com/open-feature/java-sdk/commit/ea23114d424ce042681d6b42cdc6b2e2086ccbf8)) +* **deps:** update github/codeql-action digest to eef4c44 ([#1552](https://github.com/open-feature/java-sdk/issues/1552)) ([4bae329](https://github.com/open-feature/java-sdk/commit/4bae3294b20f19550a878f331cba2377df14f7c1)) + +## [1.17.0](https://github.com/open-feature/java-sdk/compare/v1.16.0...v1.17.0) (2025-08-01) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.24.0 ([#1510](https://github.com/open-feature/java-sdk/issues/1510)) ([4881966](https://github.com/open-feature/java-sdk/commit/488196656ad0fbca5211e270bfc55e3d83fa9a2f)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.25.0 ([#1514](https://github.com/open-feature/java-sdk/issues/1514)) ([bf68cbd](https://github.com/open-feature/java-sdk/commit/bf68cbdedf6ce7218fadfe3a39df38019da8bcbb)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.26.0 ([#1516](https://github.com/open-feature/java-sdk/issues/1516)) ([1d3fab6](https://github.com/open-feature/java-sdk/commit/1d3fab6184b4ba45b3e4cee420e24be722c76946)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.27.0 ([#1530](https://github.com/open-feature/java-sdk/issues/1530)) ([c06d6d5](https://github.com/open-feature/java-sdk/commit/c06d6d588d529ae52d763a8dcf414b7aa1025d81)) +* **deps:** update dependency org.junit:junit-bom to v5.13.4 ([#1524](https://github.com/open-feature/java-sdk/issues/1524)) ([db47b7e](https://github.com/open-feature/java-sdk/commit/db47b7e8233970b0bf37dbb5679227d1917e15b7)) + + +### โœจ New Features + +* Allow Access to ImmutableMetadata Map as unmodifiable Map ([#1534](https://github.com/open-feature/java-sdk/issues/1534)) ([09e3c3f](https://github.com/open-feature/java-sdk/commit/09e3c3faf8fe6c3e8c4d46c6fa3e3a7a2dd8f146)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/checkout digest to 8edcb1b ([#1529](https://github.com/open-feature/java-sdk/issues/1529)) ([074a5ec](https://github.com/open-feature/java-sdk/commit/074a5ec52d7591e5b06801782415d1f2c930086e)) +* **deps:** update actions/setup-java digest to ae2b61d ([#1518](https://github.com/open-feature/java-sdk/issues/1518)) ([1382b36](https://github.com/open-feature/java-sdk/commit/1382b367d934feaa5effe851f8b03b02bb2482c1)) +* **deps:** update actions/setup-java digest to c190c18 ([#1508](https://github.com/open-feature/java-sdk/issues/1508)) ([908755c](https://github.com/open-feature/java-sdk/commit/908755c2c2e3abcef84f29728fd19092a9d66646)) +* **deps:** update actions/setup-java digest to e9343db ([#1535](https://github.com/open-feature/java-sdk/issues/1535)) ([7cca589](https://github.com/open-feature/java-sdk/commit/7cca589a7e2de6f3a9ec2d803dd9564205af722a)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.45.0 ([#1509](https://github.com/open-feature/java-sdk/issues/1509)) ([62738f7](https://github.com/open-feature/java-sdk/commit/62738f7f16b783eabb7325bed3ac26be086b35e4)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.46.0 ([#1522](https://github.com/open-feature/java-sdk/issues/1522)) ([844f6df](https://github.com/open-feature/java-sdk/commit/844f6df33542b927d38627f9a8ee5f9371e47aca)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.46.1 ([#1526](https://github.com/open-feature/java-sdk/issues/1526)) ([91e451b](https://github.com/open-feature/java-sdk/commit/91e451b29b10031a9697156194af1d209ee5fec6)) +* **deps:** update dependency com.google.guava:guava to v33.4.8-jre ([#1382](https://github.com/open-feature/java-sdk/issues/1382)) ([1e8f5c8](https://github.com/open-feature/java-sdk/commit/1e8f5c880c1a0e8f0ccaa7c4b7452a051973f2b6)) +* **deps:** update dependency maven to v3.9.11 ([#1519](https://github.com/open-feature/java-sdk/issues/1519)) ([cbf7a58](https://github.com/open-feature/java-sdk/commit/cbf7a5862286dc36023095208c3e865b058dacb0)) +* **deps:** update github/codeql-action digest to 03a2a17 ([#1520](https://github.com/open-feature/java-sdk/issues/1520)) ([ecc8f7e](https://github.com/open-feature/java-sdk/commit/ecc8f7e3ade314c050c67710ae96a182534b9692)) +* **deps:** update github/codeql-action digest to 0d17ea4 ([#1517](https://github.com/open-feature/java-sdk/issues/1517)) ([5b3e365](https://github.com/open-feature/java-sdk/commit/5b3e3656f6efad1f9020937bc3ea18078c4defc8)) +* **deps:** update github/codeql-action digest to 624d0bc ([#1507](https://github.com/open-feature/java-sdk/issues/1507)) ([26716a5](https://github.com/open-feature/java-sdk/commit/26716a51cfc720bdb294b50ff3759f8ae41fe410)) +* **deps:** update github/codeql-action digest to 6f936b5 ([#1515](https://github.com/open-feature/java-sdk/issues/1515)) ([006ae75](https://github.com/open-feature/java-sdk/commit/006ae75e2b1c745476dfda35113a06fc7fbceafb)) +* **deps:** update github/codeql-action digest to 701df0e ([#1528](https://github.com/open-feature/java-sdk/issues/1528)) ([b5e335c](https://github.com/open-feature/java-sdk/commit/b5e335c3ee7865f26bcd688953204280affe2834)) +* **deps:** update github/codeql-action digest to 7273f08 ([#1537](https://github.com/open-feature/java-sdk/issues/1537)) ([4addf64](https://github.com/open-feature/java-sdk/commit/4addf6458dacbc00bb599a758d87478e6d97d369)) +* **deps:** update github/codeql-action digest to 76bf77d ([#1527](https://github.com/open-feature/java-sdk/issues/1527)) ([c05757e](https://github.com/open-feature/java-sdk/commit/c05757e4895253053e49982dbe8f16ef501fd038)) +* **deps:** update github/codeql-action digest to 7710ed1 ([#1521](https://github.com/open-feature/java-sdk/issues/1521)) ([ac3344c](https://github.com/open-feature/java-sdk/commit/ac3344c7f6293ac72523a5d0c5e61d4304c0a8b1)) +* **deps:** update github/codeql-action digest to acdac9e ([#1531](https://github.com/open-feature/java-sdk/issues/1531)) ([15aaf58](https://github.com/open-feature/java-sdk/commit/15aaf5800f0fb2b8d22415fa5d9b61dacc651932)) +* **deps:** update github/codeql-action digest to b9b3b12 ([#1533](https://github.com/open-feature/java-sdk/issues/1533)) ([477d7ce](https://github.com/open-feature/java-sdk/commit/477d7ce752ecbc5b3ad13753888d5ee6b650c390)) +* **deps:** update github/codeql-action digest to eefe1b5 ([#1523](https://github.com/open-feature/java-sdk/issues/1523)) ([66215ef](https://github.com/open-feature/java-sdk/commit/66215efaf3a18eeeb4c244775d6a72725a274097)) +* **deps:** update github/codeql-action digest to f53ec7c ([#1512](https://github.com/open-feature/java-sdk/issues/1512)) ([aa05693](https://github.com/open-feature/java-sdk/commit/aa0569379bd85d11a5f91bd1078cd9f2b3b311b4)) + +## [1.16.0](https://github.com/open-feature/java-sdk/compare/v1.15.1...v1.16.0) (2025-07-07) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.23.0 ([#1466](https://github.com/open-feature/java-sdk/issues/1466)) ([50a6b16](https://github.com/open-feature/java-sdk/commit/50a6b168a7de40337aa51ef3d79d122030956cb9)) +* **deps:** update dependency org.junit:junit-bom to v5.13.1 ([#1475](https://github.com/open-feature/java-sdk/issues/1475)) ([545d6aa](https://github.com/open-feature/java-sdk/commit/545d6aac09dbc74c00a0a4e5c26f4ef80be22379)) +* **deps:** update dependency org.junit:junit-bom to v5.13.2 ([#1492](https://github.com/open-feature/java-sdk/issues/1492)) ([34b22e8](https://github.com/open-feature/java-sdk/commit/34b22e8d93a986fdb81500ab539b4d2fe038b618)) +* **deps:** update dependency org.junit:junit-bom to v5.13.3 ([#1505](https://github.com/open-feature/java-sdk/issues/1505)) ([957c0d1](https://github.com/open-feature/java-sdk/commit/957c0d1ba38ecc758c1ec164e40070ac93a01d68)) +* **deps:** update junit5 monorepo ([#1467](https://github.com/open-feature/java-sdk/issues/1467)) ([f8260a1](https://github.com/open-feature/java-sdk/commit/f8260a1c3a345c877eba95bfe41184ad11f6555e)) +* Reduce locking and concurrency issues ([#1478](https://github.com/open-feature/java-sdk/issues/1478)) ([ebea0fd](https://github.com/open-feature/java-sdk/commit/ebea0fdf1cf3e6f4d2e8aebf2dcb7c7e1f31acc2)) + + +### โœจ New Features + +* add means of awaiting event emission, fix flaky build ([#1463](https://github.com/open-feature/java-sdk/issues/1463)) ([3dd7d5d](https://github.com/open-feature/java-sdk/commit/3dd7d5d4262f1f4461e13c13a7d64d2fa8bfd764)), closes [#1449](https://github.com/open-feature/java-sdk/issues/1449) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 640a1c2 ([#1485](https://github.com/open-feature/java-sdk/issues/1485)) ([7c2af57](https://github.com/open-feature/java-sdk/commit/7c2af57a362ee11f757a431ee17eff3ee448bf6c)) +* **deps:** update actions/checkout digest to 09d2aca ([#1473](https://github.com/open-feature/java-sdk/issues/1473)) ([b5d873e](https://github.com/open-feature/java-sdk/commit/b5d873e44d3c41b42f11569b0fafccc0a002ebdd)) +* **deps:** update actions/setup-java digest to 67aec00 ([#1504](https://github.com/open-feature/java-sdk/issues/1504)) ([08f549a](https://github.com/open-feature/java-sdk/commit/08f549afd1fd26581b2a8e063832ec986c5e3267)) +* **deps:** update actions/setup-java digest to ebb356c ([#1490](https://github.com/open-feature/java-sdk/issues/1490)) ([e67f598](https://github.com/open-feature/java-sdk/commit/e67f5983573afff805a56ef18584d1a7291ccafc)) +* **deps:** update codecov/codecov-action action to v5.4.3 ([#1454](https://github.com/open-feature/java-sdk/issues/1454)) ([e337939](https://github.com/open-feature/java-sdk/commit/e3379395e6bfb0ce811d8372761a3cb015ad2cde)) +* **deps:** update dependency com.diffplug.spotless:spotless-maven-plugin to v2.44.5 ([#1462](https://github.com/open-feature/java-sdk/issues/1462)) ([40b319c](https://github.com/open-feature/java-sdk/commit/40b319c5de0461bec13f76978ae09edc958310cd)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.3.1 ([#1493](https://github.com/open-feature/java-sdk/issues/1493)) ([b64efe8](https://github.com/open-feature/java-sdk/commit/b64efe82d993defe070dfeb9aa60e740ccf757cd)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.9.3.2 ([#1496](https://github.com/open-feature/java-sdk/issues/1496)) ([fc430c3](https://github.com/open-feature/java-sdk/commit/fc430c3e1d57a532d8c0c879c3e7e25c46d4ad84)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10.24.0 ([#1458](https://github.com/open-feature/java-sdk/issues/1458)) ([dcbfd26](https://github.com/open-feature/java-sdk/commit/dcbfd265a3875271695af760fce9870e53c69f13)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10.25.0 ([#1468](https://github.com/open-feature/java-sdk/issues/1468)) ([1558a86](https://github.com/open-feature/java-sdk/commit/1558a862497c0e133d11d53ff6d7f28437653d43)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10.25.1 ([#1489](https://github.com/open-feature/java-sdk/issues/1489)) ([312b6df](https://github.com/open-feature/java-sdk/commit/312b6df5d2c891ac758bf398f8399ecd25b7597e)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10.26.0 ([#1494](https://github.com/open-feature/java-sdk/issues/1494)) ([300a705](https://github.com/open-feature/java-sdk/commit/300a705e0af959da7ed0e88e9975379ff6fc4138)) +* **deps:** update dependency com.puppycrawl.tools:checkstyle to v10.26.1 ([#1498](https://github.com/open-feature/java-sdk/issues/1498)) ([2e3b479](https://github.com/open-feature/java-sdk/commit/2e3b479cb1e8b0b65652ee813eaa2e1940d53c8e)) +* **deps:** update dependency maven to v3.9.10 ([#1474](https://github.com/open-feature/java-sdk/issues/1474)) ([4481537](https://github.com/open-feature/java-sdk/commit/4481537cebc213dcfe19bb8cd9b70a4c91a682b2)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.17.6 ([#1482](https://github.com/open-feature/java-sdk/issues/1482)) ([8e51e6f](https://github.com/open-feature/java-sdk/commit/8e51e6fe101882184a5d09be31fa65563d82c673)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.17.6 ([#1483](https://github.com/open-feature/java-sdk/issues/1483)) ([936ff60](https://github.com/open-feature/java-sdk/commit/936ff60fac471a83a7c14412d2e825b2a7f9704c)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.8 ([#1501](https://github.com/open-feature/java-sdk/issues/1501)) ([0515ad5](https://github.com/open-feature/java-sdk/commit/0515ad54c4f71863373eb1b7f429393923b27d90)) +* **deps:** update dependency org.codehaus.mojo:exec-maven-plugin to v3.5.1 ([#1461](https://github.com/open-feature/java-sdk/issues/1461)) ([b6ceff2](https://github.com/open-feature/java-sdk/commit/b6ceff2ecb0e34be2ccdb83f7f37c1177de6f27e)) +* **deps:** update dependency org.mockito:mockito-core to v5.18.0 ([#1457](https://github.com/open-feature/java-sdk/issues/1457)) ([e17b0b2](https://github.com/open-feature/java-sdk/commit/e17b0b29758ae7cdbdac9ddb2178382c55eb1277)) +* **deps:** update github/codeql-action digest to 075e08a ([#1470](https://github.com/open-feature/java-sdk/issues/1470)) ([6597de7](https://github.com/open-feature/java-sdk/commit/6597de7a98e0fae10a541a8a9b60837623c133a8)) +* **deps:** update github/codeql-action digest to 33f8489 ([#1502](https://github.com/open-feature/java-sdk/issues/1502)) ([0fd9d3d](https://github.com/open-feature/java-sdk/commit/0fd9d3dcfb1fd65197a42885b12d40a1cc152d3b)) +* **deps:** update github/codeql-action digest to 396fd27 ([#1456](https://github.com/open-feature/java-sdk/issues/1456)) ([b45a937](https://github.com/open-feature/java-sdk/commit/b45a9370173e3d3b97c78449dfc99225fb572228)) +* **deps:** update github/codeql-action digest to 3de706a ([#1481](https://github.com/open-feature/java-sdk/issues/1481)) ([99a3006](https://github.com/open-feature/java-sdk/commit/99a3006de878ab0ba1f0e61a4cb5432914425795)) +* **deps:** update github/codeql-action digest to 466d6ce ([#1477](https://github.com/open-feature/java-sdk/issues/1477)) ([0b57bca](https://github.com/open-feature/java-sdk/commit/0b57bcafc14b946000feb4a3421d73b9616e83cb)) +* **deps:** update github/codeql-action digest to 4a00331 ([#1469](https://github.com/open-feature/java-sdk/issues/1469)) ([376f81f](https://github.com/open-feature/java-sdk/commit/376f81f5c3b66d7e3e298aac30ac7544b84e7362)) +* **deps:** update github/codeql-action digest to 4c57370 ([#1497](https://github.com/open-feature/java-sdk/issues/1497)) ([49214b7](https://github.com/open-feature/java-sdk/commit/49214b7282ddde1ee16cf80f92c11cc90ef7612a)) +* **deps:** update github/codeql-action digest to 510dfa3 ([#1450](https://github.com/open-feature/java-sdk/issues/1450)) ([d9a72d2](https://github.com/open-feature/java-sdk/commit/d9a72d2aafd787a1814132f000897ad1c94181e4)) +* **deps:** update github/codeql-action digest to 57eebf6 ([#1455](https://github.com/open-feature/java-sdk/issues/1455)) ([36eed06](https://github.com/open-feature/java-sdk/commit/36eed065e763bbfa0f8f97d704202bbd219332ca)) +* **deps:** update github/codeql-action digest to 66d7255 ([#1487](https://github.com/open-feature/java-sdk/issues/1487)) ([c3eaecd](https://github.com/open-feature/java-sdk/commit/c3eaecdb8b34d3b33946bd205ee92d49584602bd)) +* **deps:** update github/codeql-action digest to 7b0fb5a ([#1459](https://github.com/open-feature/java-sdk/issues/1459)) ([6a95c00](https://github.com/open-feature/java-sdk/commit/6a95c008e975dd3c7328c32f1d7cf626bbaecfa6)) +* **deps:** update github/codeql-action digest to 7cb9b16 ([#1476](https://github.com/open-feature/java-sdk/issues/1476)) ([6cca721](https://github.com/open-feature/java-sdk/commit/6cca721be5bc6f5926fe64668a7c03728cab3cb0)) +* **deps:** update github/codeql-action digest to 7fd6215 ([#1464](https://github.com/open-feature/java-sdk/issues/1464)) ([f10aaaa](https://github.com/open-feature/java-sdk/commit/f10aaaa357581b573895f4d6e2329abb705582aa)) +* **deps:** update github/codeql-action digest to 8ef1782 ([#1495](https://github.com/open-feature/java-sdk/issues/1495)) ([86a5916](https://github.com/open-feature/java-sdk/commit/86a5916f0dc6116b5b9e5dc897ff4b8705ac01e3)) +* **deps:** update github/codeql-action digest to 9b02dc2 ([#1491](https://github.com/open-feature/java-sdk/issues/1491)) ([6f67b06](https://github.com/open-feature/java-sdk/commit/6f67b06f712c461f331681a76f5cb2c3ddb0d36b)) +* **deps:** update github/codeql-action digest to ac30a39 ([#1488](https://github.com/open-feature/java-sdk/issues/1488)) ([8fad544](https://github.com/open-feature/java-sdk/commit/8fad544b17ee08b4280d7975073d00a874c374db)) +* **deps:** update github/codeql-action digest to b1e4dc3 ([#1471](https://github.com/open-feature/java-sdk/issues/1471)) ([2dcd6a1](https://github.com/open-feature/java-sdk/commit/2dcd6a1dd0c80ee676b9860afd6a6002d0ea4aea)) +* **deps:** update github/codeql-action digest to b694213 ([#1503](https://github.com/open-feature/java-sdk/issues/1503)) ([a5d1cbc](https://github.com/open-feature/java-sdk/commit/a5d1cbced4658fadb63f362b4512bdbd68ae7d6a)) +* **deps:** update github/codeql-action digest to b86edfc ([#1453](https://github.com/open-feature/java-sdk/issues/1453)) ([b667aa3](https://github.com/open-feature/java-sdk/commit/b667aa325136b78c01867d40342f81eeb7e16f46)) +* **deps:** update github/codeql-action digest to bc02a25 ([#1460](https://github.com/open-feature/java-sdk/issues/1460)) ([5e922cf](https://github.com/open-feature/java-sdk/commit/5e922cf3efc156135563707de92e508b0a4d19f3)) +* **deps:** update github/codeql-action digest to be30325 ([#1479](https://github.com/open-feature/java-sdk/issues/1479)) ([844d5e2](https://github.com/open-feature/java-sdk/commit/844d5e244b02703b624cf75e5bf8448c07e62d3d)) +* **deps:** update github/codeql-action digest to dcc1a66 ([#1499](https://github.com/open-feature/java-sdk/issues/1499)) ([69519b1](https://github.com/open-feature/java-sdk/commit/69519b1ef7274ceae39d6746c5a5a98dc69f562f)) +* **deps:** update github/codeql-action digest to ef36b69 ([#1484](https://github.com/open-feature/java-sdk/issues/1484)) ([8bf777a](https://github.com/open-feature/java-sdk/commit/8bf777a7e99be4dfac8917b8e61cb6c23385b8ce)) +* **deps:** update io.cucumber.version to v7.23.0 ([#1465](https://github.com/open-feature/java-sdk/issues/1465)) ([2de7616](https://github.com/open-feature/java-sdk/commit/2de76166764bacd34883b13220dd0bad824c8b1a)) +* improvements to release workflow ([#1451](https://github.com/open-feature/java-sdk/issues/1451)) ([1714efe](https://github.com/open-feature/java-sdk/commit/1714efe81aa6ae025f4f8b12c9c042561498d25e)) +* migrate to new publish ([5425a34](https://github.com/open-feature/java-sdk/commit/5425a34a12baa04f9583b83fd1bfdd7e2a6ab5e8)) +* remove unneeded version information ([#1428](https://github.com/open-feature/java-sdk/issues/1428)) ([3ed65cf](https://github.com/open-feature/java-sdk/commit/3ed65cfb0cb5ee5b70793cd68a27909c81cd4fab)) +* skip tests on publish ([6194186](https://github.com/open-feature/java-sdk/commit/6194186b3e791f3cb28da24f5acb3ff96788d65e)) +* update publish env vars ([85d89ee](https://github.com/open-feature/java-sdk/commit/85d89ee79a52d960322731fb786c0f60245f0d75)) + +## [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/LICENSE b/LICENSE index 261eeb9e9..96b3dc8fc 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright OpenFeature Maintainers Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index cbb9d9f13..70b131e9a 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.18.2 ``` @@ -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.18.2' } ``` @@ -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/mvnw b/mvnw index 19529ddf8..bd8896bf2 100644 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 249bdf382..5761d9489 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,149 +1,189 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index e75dd506a..07e78eb42 100644 --- a/pom.xml +++ b/pom.xml @@ -5,16 +5,23 @@ dev.openfeature sdk - 1.14.0 + 1.18.2 + [17,) UTF-8 - 1.8 + 11 ${maven.compiler.source} - 5.11.4 + 5.20.0 + 1.2.22 **/e2e/*.java ${project.groupId}.${project.artifactId} + false + + 11 + ${settings.localRepository}/org/mockito/mockito-core/${org.mockito.version}/mockito-core-${org.mockito.version}.jar + -javaagent:${org.mockito.jar} OpenFeature Java SDK @@ -48,7 +55,7 @@ org.projectlombok lombok - 1.18.36 + 1.18.42 provided @@ -56,63 +63,65 @@ com.github.spotbugs spotbugs - 4.8.6 + 4.9.8 provided 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 org.assertj assertj-core - 3.27.3 + 3.27.6 test 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 +137,12 @@ test + + io.cucumber + cucumber-picocontainer + test + + org.simplify4u slf4j2-mock @@ -138,14 +153,14 @@ com.google.guava guava - 33.4.0-jre + 33.5.0-jre test org.awaitility awaitility - 4.2.2 + 4.3.0 test @@ -156,6 +171,38 @@ test + + com.fasterxml.jackson.core + jackson-core + test + + + + com.fasterxml.jackson.core + jackson-annotations + test + + + + com.fasterxml.jackson.core + jackson-databind + test + + + + dev.cel + cel + 0.11.1 + test + + + + com.vmlens + api + ${com.vmlens.version} + test + + @@ -167,22 +214,30 @@ net.bytebuddy byte-buddy - 1.17.0 + 1.18.1 test net.bytebuddy byte-buddy-agent - 1.17.0 + 1.18.1 test + + com.fasterxml.jackson + jackson-bom + 2.20.1 + pom + import + + io.cucumber cucumber-bom - 7.21.0 + 7.31.0 pom import @@ -190,16 +245,27 @@ org.junit junit-bom - 5.11.4 + 6.0.1 pom import - + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + select-jdk-toolchain + + + + org.cyclonedx cyclonedx-maven-plugin @@ -226,47 +292,21 @@ - - 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.1 org.apache.maven.plugins maven-surefire-plugin - 3.5.2 + 3.5.4 ${surefireArgLine} + ${org.mockito.agent.argline} + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED @@ -278,78 +318,20 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.5.2 + 3.5.4 ${surefireArgLine} + ${org.mockito.agent.argline} - - 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 maven-jar-plugin - 3.4.2 + 3.5.0 @@ -358,156 +340,234 @@ - - - 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 - - - - - - deploy + codequality true - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 - true + com.vmlens + vmlens-maven-plugin + ${com.vmlens.version} + + + test + + test + + + + **/*CT.java + + true + ${org.mockito.agent.argline} + + + + + + maven-dependency-plugin + 3.9.0 + + + verify + + analyze + + + - ossrh - https://s01.oss.sonatype.org/ - true + 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.14 + + + + 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.70 + + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.9.8.1 + + spotbugs-exclusions.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + 1.14.0 + + + + + + + com.github.spotbugs + spotbugs + 4.9.8 + + + + + run-spotbugs + verify + + check + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + checkstyle.xml + true + true + false + + + + com.puppycrawl.tools + checkstyle + 12.1.2 + + + + + validate + validate + + check + + + + + + com.diffplug.spotless + spotless-maven-plugin + 3.1.0 + + + + + + + + + .gitattributes + .gitignore + + + + + + true + 4 + + + + + + + + + true + 4 + + + + + + + + + + + + check + + + + @@ -527,7 +587,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.12.0 true all,-missing @@ -543,12 +603,35 @@ + + + + + deploy + + true + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + true + + + org.apache.maven.plugins maven-gpg-plugin - 3.2.7 + 3.2.8 sign-artifacts @@ -589,7 +672,7 @@ org.codehaus.mojo exec-maven-plugin - 3.5.0 + 3.6.2 update-test-harness-submodule @@ -608,19 +691,76 @@ + + + + + + + + + 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.4 + + + ${surefireArgLine} + + + + ${testExclusions} + + + ${skip.tests} + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + ${org.mockito.agent.argline} + ${surefireArgLine} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + - copy-evaluation-gherkin-tests - validate + default-testCompile + test-compile - exec + testCompile - - cp - - spec/specification/assets/gherkin/evaluation.feature - src/test/resources/features/ - + true @@ -632,8 +772,8 @@ - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ 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/release/m2-settings.xml b/release/m2-settings.xml index 9b7a585a3..517375160 100644 --- a/release/m2-settings.xml +++ b/release/m2-settings.xml @@ -5,5 +5,10 @@ ${env.OSSRH_USERNAME} ${env.OSSRH_PASSWORD} + + central + ${env.CENTRAL_USERNAME} + ${env.CENTRAL_PASSWORD} + diff --git a/spec b/spec index d4a9a9109..e33a15e92 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit d4a9a910946eded57cf82d6fd4921785a5e64c2b +Subproject commit e33a15e92bd0e45f0de087e7e55ee7e87f952c29 diff --git a/spotbugs-exclusions.xml b/spotbugs-exclusions.xml index 66032ad08..b841bbad4 100644 --- a/spotbugs-exclusions.xml +++ b/spotbugs-exclusions.xml @@ -49,7 +49,6 @@ - @@ -58,4 +57,4 @@ - \ No newline at end of file + 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/DefaultHookData.java b/src/main/java/dev/openfeature/sdk/DefaultHookData.java new file mode 100644 index 000000000..d0efe49d0 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/DefaultHookData.java @@ -0,0 +1,39 @@ +package dev.openfeature.sdk; + +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation of HookData. + */ +public class DefaultHookData implements HookData { + private Map data; + + @Override + public void set(String key, Object value) { + if (data == null) { + data = new HashMap<>(); + } + data.put(key, value); + } + + @Override + public Object get(String key) { + if (data == null) { + return null; + } + return data.get(key); + } + + @Override + public T get(String key, Class type) { + Object value = get(key); + if (value == null) { + return null; + } + if (!type.isInstance(value)) { + throw new ClassCastException("Value for key '" + key + "' is not of type " + type.getName()); + } + return type.cast(value); + } +} 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..4ccac184e 100644 --- a/src/main/java/dev/openfeature/sdk/EventProvider.java +++ b/src/main/java/dev/openfeature/sdk/EventProvider.java @@ -1,6 +1,11 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; 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 +19,11 @@ * * @see FeatureProvider */ +@Slf4j public abstract class EventProvider implements FeatureProvider { private EventProviderListener eventProviderListener; + private final ExecutorService emitterExecutor = + Executors.newCachedThreadPool(new ConfigurableThreadFactory("openfeature-event-emitter-thread")); void setEventProviderListener(EventProviderListener eventProviderListener) { this.eventProviderListener = eventProviderListener; @@ -46,19 +54,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 +112,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 +123,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 +133,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 +143,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..0b446c6b2 100644 --- a/src/main/java/dev/openfeature/sdk/EventSupport.java +++ b/src/main/java/dev/openfeature/sdk/EventSupport.java @@ -1,12 +1,13 @@ package dev.openfeature.sdk; -import java.util.ArrayList; -import java.util.List; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; +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 +20,15 @@ @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(new ConfigurableThreadFactory("openfeature-event-handler-thread")); /** * Run all the event handlers associated with this domain. @@ -40,11 +39,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 +67,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 +87,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 +158,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/HookContext.java b/src/main/java/dev/openfeature/sdk/HookContext.java index e14eeb643..8d4d2e13a 100644 --- a/src/main/java/dev/openfeature/sdk/HookContext.java +++ b/src/main/java/dev/openfeature/sdk/HookContext.java @@ -1,32 +1,56 @@ package dev.openfeature.sdk; -import lombok.Builder; +import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Objects; +import lombok.EqualsAndHashCode; import lombok.NonNull; -import lombok.Value; -import lombok.With; +import lombok.ToString; /** * A data class to hold immutable context that {@link Hook} instances use. * * @param the type for the flag being evaluated */ -@Value -@Builder -@With -public class HookContext { - @NonNull String flagKey; +@EqualsAndHashCode +@ToString +public final class HookContext { + private final SharedHookContext sharedContext; + private EvaluationContext ctx; + private final HookData hookData; - @NonNull FlagValueType type; - - @NonNull T defaultValue; - - @NonNull EvaluationContext ctx; + HookContext(@NonNull SharedHookContext sharedContext, EvaluationContext evaluationContext, HookData hookData) { + this.sharedContext = sharedContext; + ctx = evaluationContext; + this.hookData = hookData; + } - ClientMetadata clientMetadata; - Metadata providerMetadata; + /** + * Obsolete constructor. + * This constructor is retained for binary compatibility but is no longer part of the public API. + * + * @param flagKey feature flag key + * @param type flag value type + * @param clientMetadata info on which client is calling + * @param providerMetadata info on the provider + * @param ctx Evaluation Context for the request + * @param defaultValue Fallback value + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @Deprecated + HookContext( + @NonNull String flagKey, + @NonNull FlagValueType type, + @NonNull T defaultValue, + @NonNull EvaluationContext ctx, + ClientMetadata clientMetadata, + Metadata providerMetadata, + HookData hookData) { + this(new SharedHookContext<>(flagKey, type, clientMetadata, providerMetadata, defaultValue), ctx, hookData); + } /** - * Builds a {@link HookContext} instances from request data. + * Builds {@link HookContext} instances from request data. * * @param key feature flag key * @param type flag value type @@ -36,7 +60,9 @@ public class HookContext { * @param defaultValue Fallback value * @param type that the flag is evaluating against * @return resulting context for hook + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. */ + @Deprecated public static HookContext from( String key, FlagValueType type, @@ -51,6 +77,286 @@ public static HookContext from( .providerMetadata(providerMetadata) .ctx(ctx) .defaultValue(defaultValue) + .hookData(null) .build(); } + + /** + * Creates a new builder for {@link HookContext}. + * + * @param the type for the flag being evaluated + * @return a new builder + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @Deprecated + public static HookContextBuilder builder() { + return new HookContextBuilder(); + } + + public @NonNull String getFlagKey() { + return sharedContext.getFlagKey(); + } + + public @NonNull FlagValueType getType() { + return sharedContext.getType(); + } + + public @NonNull T getDefaultValue() { + return sharedContext.getDefaultValue(); + } + + public @NonNull EvaluationContext getCtx() { + return this.ctx; + } + + public ClientMetadata getClientMetadata() { + return sharedContext.getClientMetadata(); + } + + public Metadata getProviderMetadata() { + return sharedContext.getProviderMetadata(); + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Intentional exposure of hookData") + public HookData getHookData() { + return this.hookData; + } + + void setCtx(@NonNull EvaluationContext ctx) { + this.ctx = ctx; + } + + /** + * Returns a new HookContext with the provided flagKey if it is different from the current one. + * + * @param flagKey new flag key + * @return new HookContext with updated flagKey or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withFlagKey(@NonNull String flagKey) { + return Objects.equals(this.getFlagKey(), flagKey) + ? this + : new HookContext( + flagKey, + this.getType(), + this.getDefaultValue(), + this.getCtx(), + this.getClientMetadata(), + this.getProviderMetadata(), + this.hookData); + } + + /** + * Returns a new HookContext with the provided type if it is different from the current one. + * + * @param type new flag value type + * @return new HookContext with updated type or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withType(@NonNull FlagValueType type) { + return this.getType() == type + ? this + : new HookContext( + this.getFlagKey(), + type, + this.getDefaultValue(), + this.getCtx(), + this.getClientMetadata(), + this.getProviderMetadata(), + this.hookData); + } + + /** + * Returns a new HookContext with the provided defaultValue if it is different from the current one. + * + * @param defaultValue new default value + * @return new HookContext with updated defaultValue or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withDefaultValue(@NonNull T defaultValue) { + return this.getDefaultValue() == defaultValue + ? this + : new HookContext( + this.getFlagKey(), + this.getType(), + defaultValue, + this.getCtx(), + this.getClientMetadata(), + this.getProviderMetadata(), + this.hookData); + } + + /** + * Returns a new HookContext with the provided ctx if it is different from the current one. + * + * @param ctx new evaluation context + * @return new HookContext with updated ctx or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withCtx(@NonNull EvaluationContext ctx) { + return this.ctx == ctx + ? this + : new HookContext( + this.getFlagKey(), + this.getType(), + this.getDefaultValue(), + ctx, + this.getClientMetadata(), + this.getProviderMetadata(), + this.hookData); + } + + /** + * Returns a new HookContext with the provided clientMetadata if it is different from the current one. + * + * @param clientMetadata new client metadata + * @return new HookContext with updated clientMetadata or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withClientMetadata(ClientMetadata clientMetadata) { + return this.getClientMetadata() == clientMetadata + ? this + : new HookContext( + this.getFlagKey(), + this.getType(), + this.getDefaultValue(), + this.getCtx(), + clientMetadata, + this.getProviderMetadata(), + this.hookData); + } + + /** + * Returns a new HookContext with the provided providerMetadata if it is different from the current one. + * + * @param providerMetadata new provider metadata + * @return new HookContext with updated providerMetadata or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withProviderMetadata(Metadata providerMetadata) { + return this.getProviderMetadata() == providerMetadata + ? this + : new HookContext( + this.getFlagKey(), + this.getType(), + this.getDefaultValue(), + this.getCtx(), + this.getClientMetadata(), + providerMetadata, + this.hookData); + } + + /** + * Returns a new HookContext with the provided hookData if it is different from the current one. + * + * @param hookData new hook data + * @return new HookContext with updated hookData or the same instance if unchanged + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @ExcludeFromGeneratedCoverageReport + @Deprecated + public HookContext withHookData(HookData hookData) { + return this.hookData == hookData + ? this + : new HookContext( + this.getFlagKey(), + this.getType(), + this.getDefaultValue(), + this.getCtx(), + this.getClientMetadata(), + this.getProviderMetadata(), + hookData); + } + + /** + * Builder for HookContext. + * + * @param The flag type. + * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances. + */ + @Deprecated + @ToString + public static class HookContextBuilder { + private String flagKey; + private FlagValueType type; + private T defaultValue; + private EvaluationContext ctx; + private ClientMetadata clientMetadata; + private Metadata providerMetadata; + private HookData hookData; + + HookContextBuilder() {} + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder flagKey(@NonNull String flagKey) { + this.flagKey = flagKey; + return this; + } + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder type(@NonNull FlagValueType type) { + this.type = type; + return this; + } + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder defaultValue(@NonNull T defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder ctx(@NonNull EvaluationContext ctx) { + this.ctx = ctx; + return this; + } + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder clientMetadata(ClientMetadata clientMetadata) { + this.clientMetadata = clientMetadata; + return this; + } + + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder providerMetadata(Metadata providerMetadata) { + this.providerMetadata = providerMetadata; + return this; + } + + @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Intentional exposure of hookData") + @ExcludeFromGeneratedCoverageReport + public HookContextBuilder hookData(HookData hookData) { + this.hookData = hookData; + return this; + } + + /** + * Builds the HookContext instance. + * + * @return a new HookContext + */ + @ExcludeFromGeneratedCoverageReport + public HookContext build() { + return new HookContext( + this.flagKey, + this.type, + this.defaultValue, + this.ctx, + this.clientMetadata, + this.providerMetadata, + this.hookData); + } + } } diff --git a/src/main/java/dev/openfeature/sdk/HookData.java b/src/main/java/dev/openfeature/sdk/HookData.java new file mode 100644 index 000000000..bd2c5dba9 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/HookData.java @@ -0,0 +1,35 @@ +package dev.openfeature.sdk; + +/** + * Hook data provides a way for hooks to maintain state across their execution stages. + * Each hook instance gets its own isolated data store that persists only for the duration + * of a single flag evaluation. + */ +public interface HookData { + /** + * Sets a value for the given key. + * + * @param key the key to store the value under + * @param value the value to store + */ + void set(String key, Object value); + + /** + * Gets the value for the given key. + * + * @param key the key to retrieve the value for + * @return the value, or null if not found + */ + Object get(String key); + + /** + * Gets the value for the given key, cast to the specified type. + * + * @param the type to cast to + * @param key the key to retrieve the value for + * @param type the class to cast to + * @return the value cast to the specified type, or null if not found + * @throws ClassCastException if the value cannot be cast to the specified type + */ + T get(String key, Class type); +} diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index 73518ee8e..c7a7630da 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -3,99 +3,124 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.Consumer; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +/** + * Helper class to run hooks. Initialize {@link HookSupportData} by calling setHooks, setHookContexts + * & updateEvaluationContext in this exact order. + */ @Slf4j -@RequiredArgsConstructor -@SuppressWarnings({"unchecked", "rawtypes"}) class HookSupport { - public EvaluationContext beforeHooks( - FlagValueType flagValueType, HookContext hookCtx, List hooks, Map hints) { - return callBeforeHooks(flagValueType, hookCtx, hooks, hints); + /** + * Sets the {@link Hook}-{@link HookContext}-{@link Pair} list in the given data object with {@link HookContext} + * set to null. Filters hooks by supported {@link FlagValueType}. + * + * @param hookSupportData the data object to modify + * @param hooks the hooks to set + * @param type the flag value type to filter unsupported hooks + */ + public void setHooks(HookSupportData hookSupportData, List hooks, FlagValueType type) { + List> hookContextPairs = new ArrayList<>(); + for (Hook hook : hooks) { + if (hook.supportsFlagValueType(type)) { + hookContextPairs.add(Pair.of(hook, null)); + } + } + hookSupportData.hooks = hookContextPairs; } - public void afterHooks( - FlagValueType flagValueType, - HookContext hookContext, - FlagEvaluationDetails details, - List hooks, - Map hints) { - executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints)); + /** + * Creates & sets a {@link HookContext} for every {@link Hook}-{@link HookContext}-{@link Pair} + * in the given data object with a new {@link HookData} instance. + * + * @param hookSupportData the data object to modify + * @param sharedContext the shared context from which the new {@link HookContext} is created + */ + public void setHookContexts(HookSupportData hookSupportData, SharedHookContext sharedContext) { + for (int i = 0; i < hookSupportData.hooks.size(); i++) { + Pair hookContextPair = hookSupportData.hooks.get(i); + HookContext curHookContext = sharedContext.hookContextFor(null, new DefaultHookData()); + hookContextPair.setValue(curHookContext); + } } - public void afterAllHooks( - FlagValueType flagValueType, - HookContext hookCtx, - FlagEvaluationDetails details, - List hooks, - Map hints) { - executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, details, hints)); + /** + * Updates the evaluation context in the given data object's eval context and each hooks eval context. + * + * @param hookSupportData the data object to modify + * @param evaluationContext the new context to set + */ + public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationContext evaluationContext) { + hookSupportData.evaluationContext = evaluationContext; + if (hookSupportData.hooks != null) { + for (Pair hookContextPair : hookSupportData.hooks) { + var curHookContext = hookContextPair.getValue(); + if (curHookContext != null) { + curHookContext.setCtx(evaluationContext); + } + } + } } - public void errorHooks( - FlagValueType flagValueType, - HookContext hookCtx, - Exception e, - List hooks, - Map hints) { - executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints)); - } + public void executeBeforeHooks(HookSupportData data) { + // These traverse backwards from normal. + List> reversedHooks = new ArrayList<>(data.getHooks()); + Collections.reverse(reversedHooks); - private void executeHooks( - FlagValueType flagValueType, List hooks, String hookMethod, Consumer> hookCode) { - if (hooks != null) { - for (Hook hook : hooks) { - if (hook.supportsFlagValueType(flagValueType)) { - executeChecked(hook, hookCode, hookMethod); - } + for (Pair hookContextPair : reversedHooks) { + var hook = hookContextPair.getKey(); + var hookContext = hookContextPair.getValue(); + + Optional returnedEvalContext = Optional.ofNullable( + hook.before(hookContext, data.getHints())) + .orElse(Optional.empty()); + if (returnedEvalContext.isPresent()) { + // update shared evaluation context for all hooks + updateEvaluationContext(data, data.getEvaluationContext().merge(returnedEvalContext.get())); } } } - // before, error, and finally hooks shouldn't throw - private void executeChecked(Hook hook, Consumer> hookCode, String hookMethod) { - try { - hookCode.accept(hook); - } catch (Exception exception) { - log.error( - "Unhandled exception when running {} hook {} (only 'after' hooks should throw)", - hookMethod, - hook.getClass(), - exception); + public void executeErrorHooks(HookSupportData data, Exception error) { + for (Pair hookContextPair : data.getHooks()) { + var hook = hookContextPair.getKey(); + var hookContext = hookContextPair.getValue(); + try { + hook.error(hookContext, error, data.getHints()); + } catch (Exception e) { + log.error( + "Unhandled exception when running {} hook {} (only 'after' hooks should throw)", + "error", + hook.getClass(), + e); + } } } // after hooks can throw in order to do validation - private void executeHooksUnchecked(FlagValueType flagValueType, List hooks, Consumer> hookCode) { - if (hooks != null) { - for (Hook hook : hooks) { - if (hook.supportsFlagValueType(flagValueType)) { - hookCode.accept(hook); - } - } + public void executeAfterHooks(HookSupportData data, FlagEvaluationDetails details) { + for (Pair hookContextPair : data.getHooks()) { + var hook = hookContextPair.getKey(); + var hookContext = hookContextPair.getValue(); + hook.after(hookContext, details, data.getHints()); } } - private EvaluationContext callBeforeHooks( - FlagValueType flagValueType, HookContext hookCtx, List hooks, Map hints) { - // These traverse backwards from normal. - List reversedHooks = new ArrayList<>(hooks); - Collections.reverse(reversedHooks); - EvaluationContext context = hookCtx.getCtx(); - for (Hook hook : reversedHooks) { - if (hook.supportsFlagValueType(flagValueType)) { - Optional optional = - Optional.ofNullable(hook.before(hookCtx, hints)).orElse(Optional.empty()); - if (optional.isPresent()) { - context = context.merge(optional.get()); - } + public void executeAfterAllHooks(HookSupportData data, FlagEvaluationDetails details) { + for (Pair hookContextPair : data.getHooks()) { + var hook = hookContextPair.getKey(); + var hookContext = hookContextPair.getValue(); + try { + hook.finallyAfter(hookContext, details, data.getHints()); + } catch (Exception e) { + log.error( + "Unhandled exception when running {} hook {} (only 'after' hooks should throw)", + "finally", + hook.getClass(), + e); } } - return context; } } diff --git a/src/main/java/dev/openfeature/sdk/HookSupportData.java b/src/main/java/dev/openfeature/sdk/HookSupportData.java new file mode 100644 index 000000000..2d3346ba1 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/HookSupportData.java @@ -0,0 +1,18 @@ +package dev.openfeature.sdk; + +import java.util.List; +import java.util.Map; +import lombok.Getter; + +/** + * Encapsulates data for hook execution per flag evaluation. + */ +@Getter +class HookSupportData { + + List> hooks; + EvaluationContext evaluationContext; + Map hints; + + HookSupportData() {} +} diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index 23a452e08..e4916dfca 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,9 +16,12 @@ * not be modified after instantiation. */ @ToString +@EqualsAndHashCode @SuppressWarnings("PMD.BeanMembersShouldSerialize") public final class ImmutableContext implements EvaluationContext { + public static final ImmutableContext EMPTY = new ImmutableContext(); + @Delegate(excludes = DelegateExclusions.class) private final ImmutableStructure structure; diff --git a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java index c2b6f5838..945e0ea17 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import lombok.EqualsAndHashCode; @@ -12,6 +13,8 @@ @Slf4j @EqualsAndHashCode public class ImmutableMetadata { + public static final ImmutableMetadata EMPTY = new ImmutableMetadata(Collections.emptyMap()); + private final Map metadata; private ImmutableMetadata(Map metadata) { @@ -97,6 +100,18 @@ public T getValue(final String key, final Class type) { } } + public Map asUnmodifiableMap() { + return Collections.unmodifiableMap(metadata); + } + + 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/ObjectHook.java b/src/main/java/dev/openfeature/sdk/ObjectHook.java new file mode 100644 index 000000000..ad3af6444 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ObjectHook.java @@ -0,0 +1,15 @@ +package dev.openfeature.sdk; + +/** + * An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic + * to the lifecycle of flag evaluation. + * + * @see Hook + */ +public interface ObjectHook extends Hook { + + @Override + default boolean supportsFlagValueType(FlagValueType flagValueType) { + return FlagValueType.OBJECT == flagValueType; + } +} diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 9175a7cd7..6d0d8feb4 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -5,9 +5,12 @@ import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; @@ -21,15 +24,15 @@ public class OpenFeatureAPI implements EventBus { // package-private multi-read/single-write lock static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock(); - private final List apiHooks; + private final ConcurrentLinkedQueue apiHooks; private ProviderRepository providerRepository; private EventSupport eventSupport; - private EvaluationContext evaluationContext; + private final AtomicReference evaluationContext = new AtomicReference<>(); private TransactionContextPropagator transactionContextPropagator; protected OpenFeatureAPI() { - apiHooks = new ArrayList<>(); - providerRepository = new ProviderRepository(); + apiHooks = new ConcurrentLinkedQueue<>(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); transactionContextPropagator = new NoOpTransactionContextPropagator(); } @@ -115,9 +118,7 @@ public Client getClient(String domain, String version) { * @return api instance */ public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.evaluationContext = evaluationContext; - } + this.evaluationContext.set(evaluationContext); return this; } @@ -127,16 +128,14 @@ public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) * @return evaluation context */ public EvaluationContext getEvaluationContext() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { - return this.evaluationContext; - } + return evaluationContext.get(); } /** * Return the transaction context propagator. */ public TransactionContextPropagator getTransactionContextPropagator() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) { return this.transactionContextPropagator; } } @@ -150,7 +149,7 @@ public void setTransactionContextPropagator(TransactionContextPropagator transac if (transactionContextPropagator == null) { throw new IllegalArgumentException("Transaction context propagator cannot be null"); } - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { this.transactionContextPropagator = transactionContextPropagator; } } @@ -176,7 +175,7 @@ public void setTransactionContext(EvaluationContext evaluationContext) { * Set the default provider. */ public void setProvider(FeatureProvider provider) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( provider, this::attachEventProvider, @@ -194,7 +193,7 @@ public void setProvider(FeatureProvider provider) { * @param provider The provider to set. */ public void setProvider(String domain, FeatureProvider provider) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( domain, provider, @@ -207,10 +206,16 @@ 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()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( provider, this::attachEventProvider, @@ -224,11 +229,15 @@ 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()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( domain, provider, @@ -242,9 +251,7 @@ public void setProviderAndWait(String domain, FeatureProvider provider) throws O private void attachEventProvider(FeatureProvider provider) { if (provider instanceof EventProvider) { - ((EventProvider) provider).attach((p, event, details) -> { - runHandlersForProvider(p, event, details); - }); + ((EventProvider) provider).attach(this::runHandlersForProvider); } } @@ -297,9 +304,7 @@ public FeatureProvider getProvider(String domain) { * @param hooks The hook to add. */ public void addHooks(Hook... hooks) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.apiHooks.addAll(Arrays.asList(hooks)); - } + this.apiHooks.addAll(Arrays.asList(hooks)); } /** @@ -308,18 +313,23 @@ public void addHooks(Hook... hooks) { * @return A list of {@link Hook}s. */ public List getHooks() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { - return this.apiHooks; - } + return new ArrayList<>(this.apiHooks); + } + + /** + * Returns a reference to the collection of {@link Hook}s. + * + * @return The collection of {@link Hook}s. + */ + Collection getMutableHooks() { + return this.apiHooks; } /** * Removes all hooks. */ public void clearHooks() { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.apiHooks.clear(); - } + this.apiHooks.clear(); } /** @@ -329,11 +339,11 @@ public void clearHooks() { * Once shut down is complete, API is reset and ready to use again. */ public void shutdown() { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.shutdown(); eventSupport.shutdown(); - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); } } @@ -375,7 +385,7 @@ public OpenFeatureAPI onProviderError(Consumer handler) { */ @Override public OpenFeatureAPI on(ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { this.eventSupport.addGlobalHandler(event, handler); return this; } @@ -386,18 +396,20 @@ public OpenFeatureAPI on(ProviderEvent event, Consumer handler) { */ @Override public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer handler) { - this.eventSupport.removeGlobalHandler(event, handler); + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { + this.eventSupport.removeGlobalHandler(event, handler); + } return this; } void removeHandler(String domain, ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { eventSupport.removeClientHandler(domain, event, handler); } } void addHandler(String domain, ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { // if the provider is in the state associated with event, run immediately if (Optional.ofNullable(this.providerRepository.getProviderState(domain)) .orElse(ProviderState.READY) @@ -421,32 +433,28 @@ FeatureProviderStateManager getFeatureProviderStateManager(String domain) { * @param details the event details */ private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) { List domainsForProvider = providerRepository.getDomainsForProvider(provider); final String providerName = Optional.ofNullable(provider.getMetadata()) - .map(metadata -> metadata.getName()) + .map(Metadata::getName) .orElse(null); // run the global handlers eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName)); // run the handlers associated with domains for this provider - domainsForProvider.forEach(domain -> { - eventSupport.runClientHandlers( - domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)); - }); + domainsForProvider.forEach(domain -> eventSupport.runClientHandlers( + domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain))); if (providerRepository.isDefaultProvider(provider)) { // run handlers for clients that have no bound providers (since this is the default) Set allDomainNames = eventSupport.getAllDomainNames(); Set boundDomains = providerRepository.getAllBoundDomains(); allDomainNames.removeAll(boundDomains); - allDomainNames.forEach(domain -> { - eventSupport.runClientHandlers( - domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)); - }); + allDomainNames.forEach(domain -> eventSupport.runClientHandlers( + domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain))); } } } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 66f25f60a..614bc1e34 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -5,9 +5,8 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; -import dev.openfeature.sdk.internal.AutoCloseableLock; -import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; import dev.openfeature.sdk.internal.ObjectUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -15,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -46,11 +47,10 @@ public class OpenFeatureClient implements Client { @Getter private final String version; - private final List clientHooks; + private final ConcurrentLinkedQueue clientHooks; + private final AtomicReference evaluationContext = new AtomicReference<>(); + private final HookSupport hookSupport; - AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock(); - AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock(); - private EvaluationContext evaluationContext; /** * Deprecated public constructor. Use OpenFeature.API.getClient() instead. @@ -68,8 +68,8 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String ve this.openfeatureApi = openFeatureAPI; this.domain = domain; this.version = version; - this.clientHooks = new ArrayList<>(); this.hookSupport = new HookSupport(); + this.clientHooks = new ConcurrentLinkedQueue<>(); } /** @@ -125,9 +125,7 @@ public void track(String trackingEventName, EvaluationContext context, TrackingE */ @Override public OpenFeatureClient addHooks(Hook... hooks) { - try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) { - this.clientHooks.addAll(Arrays.asList(hooks)); - } + this.clientHooks.addAll(Arrays.asList(hooks)); return this; } @@ -136,9 +134,7 @@ public OpenFeatureClient addHooks(Hook... hooks) { */ @Override public List getHooks() { - try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) { - return this.clientHooks; - } + return new ArrayList<>(this.clientHooks); } /** @@ -146,9 +142,7 @@ public List getHooks() { */ @Override public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) { - try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) { - this.evaluationContext = evaluationContext; - } + this.evaluationContext.set(evaluationContext); return this; } @@ -157,67 +151,65 @@ public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContex */ @Override public EvaluationContext getEvaluationContext() { - try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) { - return this.evaluationContext; - } + return this.evaluationContext.get(); } + @SuppressFBWarnings( + value = {"REC_CATCH_EXCEPTION"}, + justification = "We don't want to allow any exception to reach the user. " + + "Instead, we return an evaluation result with the appropriate error code.") private FlagEvaluationDetails evaluateFlag( FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) { - FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull( - options, () -> FlagEvaluationOptions.builder().build()); - Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); - FlagEvaluationDetails details = null; - List mergedHooks = null; - HookContext afterHookContext = null; - FeatureProvider provider; + HookSupportData hookSupportData = new HookSupportData(); + + var flagOptions = ObjectUtils.defaultIfNull( + options, () -> FlagEvaluationOptions.builder().build()); + hookSupportData.hints = Collections.unmodifiableMap(flagOptions.getHookHints()); try { - FeatureProviderStateManager stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); + final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); // provider must be accessed once to maintain a consistent reference - provider = stateManager.getProvider(); - ProviderState state = stateManager.getState(); + final var provider = stateManager.getProvider(); + final var state = stateManager.getState(); + + // Hooks are initialized as early as possible to enable the execution of error stages + var mergedHooks = ObjectUtils.merge( + provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks()); + hookSupport.setHooks(hookSupportData, mergedHooks, type); + + var sharedHookContext = + new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue); + hookSupport.setHookContexts(hookSupportData, sharedHookContext); + + var evalContext = mergeEvaluationContext(ctx); + hookSupport.updateEvaluationContext(hookSupportData, evalContext); + + hookSupport.executeBeforeHooks(hookSupportData); + + // "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"); + throw new ProviderNotReadyError("Provider not yet initialized"); } if (ProviderState.FATAL.equals(state)) { - throw new FatalError("provider is in an irrecoverable error state"); + throw new FatalError("Provider is in an irrecoverable error state"); } - mergedHooks = ObjectUtils.merge( - provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); - - EvaluationContext mergedCtx = hookSupport.beforeHooks( - type, - HookContext.from( - key, - type, - this.getMetadata(), - provider.getMetadata(), - mergeEvaluationContext(ctx), - defaultValue), - mergedHooks, - hints); - - afterHookContext = - HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), mergedCtx, defaultValue); - - ProviderEvaluation providerEval = - (ProviderEvaluation) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx); + var providerEval = (ProviderEvaluation) + createProviderEvaluation(type, key, defaultValue, provider, hookSupportData.getEvaluationContext()); details = FlagEvaluationDetails.from(providerEval, key); if (details.getErrorCode() != null) { - OpenFeatureError error = + var error = ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage()); enrichDetailsWithErrorDefaults(defaultValue, details); - hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints); + hookSupport.executeErrorHooks(hookSupportData, error); } else { - hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints); + hookSupport.executeAfterHooks(hookSupportData, details); } } 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()); @@ -226,9 +218,13 @@ private FlagEvaluationDetails evaluateFlag( } details.setErrorMessage(e.getMessage()); enrichDetailsWithErrorDefaults(defaultValue, details); - hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints); + if (hookSupportData.getHooks() != null) { + hookSupport.executeErrorHooks(hookSupportData, e); + } } finally { - hookSupport.afterAllHooks(type, afterHookContext, details, mergedHooks, hints); + if (hookSupportData.getHooks() != null) { + hookSupport.executeAfterAllHooks(hookSupportData, details); + } } return details; @@ -262,7 +258,7 @@ private void invokeTrack(String trackingEventName, EvaluationContext context, Tr */ private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) { final EvaluationContext apiContext = openfeatureApi.getEvaluationContext(); - final EvaluationContext clientContext = this.getEvaluationContext(); + final EvaluationContext clientContext = evaluationContext.get(); final EvaluationContext transactionContext = openfeatureApi.getTransactionContext(); return mergeContextMaps(apiContext, transactionContext, clientContext, invocationContext); } @@ -507,7 +503,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 +512,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/Pair.java b/src/main/java/dev/openfeature/sdk/Pair.java new file mode 100644 index 000000000..765be9f2d --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/Pair.java @@ -0,0 +1,29 @@ +package dev.openfeature.sdk; + +import lombok.Setter; +import lombok.ToString; + +@ToString +class Pair { + private final K key; + + @Setter + private V value; + + private Pair(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public static Pair of(K key, V value) { + return new Pair<>(key, value); + } +} diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index bec866820..147074a58 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -2,6 +2,7 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; +import dev.openfeature.sdk.internal.ConfigurableThreadFactory; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,12 +23,14 @@ class ProviderRepository { private final Map stateManagers = new ConcurrentHashMap<>(); private final AtomicReference defaultStateManger = new AtomicReference<>(new FeatureProviderStateManager(new NoOpProvider())); - private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> { - final Thread thread = new Thread(runnable); - thread.setDaemon(true); - return thread; - }); + private final ExecutorService taskExecutor = + Executors.newCachedThreadPool(new ConfigurableThreadFactory("openfeature-provider-thread", true)); private final Object registerStateManagerLock = new Object(); + private final OpenFeatureAPI openFeatureAPI; + + public ProviderRepository(OpenFeatureAPI openFeatureAPI) { + this.openFeatureAPI = openFeatureAPI; + } FeatureProviderStateManager getFeatureProviderStateManager() { return defaultStateManger.get(); @@ -205,7 +208,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/SharedHookContext.java b/src/main/java/dev/openfeature/sdk/SharedHookContext.java new file mode 100644 index 000000000..8faab37b2 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/SharedHookContext.java @@ -0,0 +1,32 @@ +package dev.openfeature.sdk; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +class SharedHookContext { + + private final String flagKey; + private final FlagValueType type; + private final ClientMetadata clientMetadata; + private final Metadata providerMetadata; + private final T defaultValue; + + public SharedHookContext( + String flagKey, + FlagValueType type, + ClientMetadata clientMetadata, + Metadata providerMetadata, + T defaultValue) { + this.flagKey = flagKey; + this.type = type; + this.clientMetadata = clientMetadata; + this.providerMetadata = providerMetadata; + this.defaultValue = defaultValue; + } + + public HookContext hookContextFor(EvaluationContext evaluationContext, HookData hookData) { + return new HookContext<>(this, evaluationContext, hookData); + } +} 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/internal/ConfigurableThreadFactory.java b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java new file mode 100644 index 000000000..8d5e77db8 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/internal/ConfigurableThreadFactory.java @@ -0,0 +1,43 @@ +package dev.openfeature.sdk.internal; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A configurable thread factory for internal use in the SDK. + * Allows daemon or non-daemon threads to be created with a custom name prefix. + */ +public final class ConfigurableThreadFactory implements ThreadFactory { + + private final AtomicInteger counter = new AtomicInteger(); + private final String namePrefix; + private final boolean daemon; + + /** + * {@link ConfigurableThreadFactory}'s constructor. + * + * @param namePrefix Prefix used for setting the new thread's name. + */ + public ConfigurableThreadFactory(String namePrefix) { + this(namePrefix, false); + } + + /** + * {@link ConfigurableThreadFactory}'s constructor. + * + * @param namePrefix Prefix used for setting the new thread's name. + * @param daemon Whether daemon or non-daemon threads will be created. + */ + public ConfigurableThreadFactory(String namePrefix, boolean daemon) { + this.namePrefix = namePrefix; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable runnable) { + final Thread thread = new Thread(runnable); + thread.setDaemon(daemon); + thread.setName(namePrefix + "-" + counter.incrementAndGet()); + return thread; + } +} diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java index b367820c2..86a9ddd70 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java +++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk.internal; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -64,9 +65,9 @@ public static T defaultIfNull(T source, Supplier defaultValue) { * @return resulting object */ @SafeVarargs - public static List merge(List... sources) { + public static List merge(Collection... sources) { List merged = new ArrayList<>(); - for (List source : sources) { + for (Collection source : sources) { merged.addAll(source); } return merged; 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..4422dc51f 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,6 @@ public class Flag { private String defaultVariant; private ContextEvaluator contextEvaluator; + private ImmutableMetadata flagMetadata; + private boolean disabled; } 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..1773ae8a8 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -97,36 +97,37 @@ public void updateFlag(String flagKey, Flag newFlag) { @Override public ProviderEvaluation getBooleanEvaluation( String key, Boolean defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, evaluationContext, Boolean.class); + return getEvaluation(key, defaultValue, evaluationContext, Boolean.class); } @Override public ProviderEvaluation getStringEvaluation( String key, String defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, evaluationContext, String.class); + return getEvaluation(key, defaultValue, evaluationContext, String.class); } @Override public ProviderEvaluation getIntegerEvaluation( String key, Integer defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, evaluationContext, Integer.class); + return getEvaluation(key, defaultValue, evaluationContext, Integer.class); } @Override public ProviderEvaluation getDoubleEvaluation( String key, Double defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, evaluationContext, Double.class); + return getEvaluation(key, defaultValue, evaluationContext, Double.class); } @SneakyThrows @Override public ProviderEvaluation getObjectEvaluation( String key, Value defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, evaluationContext, Value.class); + return getEvaluation(key, defaultValue, evaluationContext, Value.class); } private ProviderEvaluation getEvaluation( - String key, EvaluationContext evaluationContext, Class expectedType) throws OpenFeatureError { + String key, T defaultValue, EvaluationContext evaluationContext, Class expectedType) + throws OpenFeatureError { if (!ProviderState.READY.equals(state)) { if (ProviderState.NOT_READY.equals(state)) { throw new ProviderNotReadyError("provider not yet initialized"); @@ -138,11 +139,28 @@ private ProviderEvaluation getEvaluation( } Flag flag = flags.get(key); if (flag == null) { - throw new FlagNotFoundError("flag " + key + "not found"); + throw new FlagNotFoundError("flag " + key + " not found"); + } + if (flag.isDisabled()) { + return ProviderEvaluation.builder() + .reason(Reason.DISABLED.name()) + .value(defaultValue) + .flagMetadata(flag.getFlagMetadata()) + .build(); } T value; + Reason reason = Reason.STATIC; if (flag.getContextEvaluator() != null) { - value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext); + try { + value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext); + reason = Reason.TARGETING_MATCH; + } catch (Exception e) { + value = null; + } + if (value == null) { + value = (T) flag.getVariants().get(flag.getDefaultVariant()); + reason = Reason.DEFAULT; + } } else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) { throw new TypeMismatchError("flag " + key + "is not of expected type"); } else { @@ -151,7 +169,8 @@ private ProviderEvaluation getEvaluation( return ProviderEvaluation.builder() .value(value) .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) + .reason(reason.toString()) + .flagMetadata(flag.getFlagMetadata()) .build(); } } diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java deleted file mode 100644 index 2f214d8ac..000000000 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.openfeature.sdk; - -import dev.openfeature.sdk.exceptions.FlagNotFoundError; - -public class AlwaysBrokenProvider implements FeatureProvider { - - private final String name = "always broken"; - - @Override - public Metadata getMetadata() { - return () -> name; - } - - @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - } - - @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - } - - @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - } - - @Override - public ProviderEvaluation getObjectEvaluation( - String key, Value defaultValue, EvaluationContext invocationContext) { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - } -} diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java deleted file mode 100644 index 8f304eaac..000000000 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.openfeature.sdk; - -import dev.openfeature.sdk.exceptions.FlagNotFoundError; - -public class AlwaysBrokenWithDetailsProvider implements FeatureProvider { - - @Override - public Metadata getMetadata() { - return () -> { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - }; - } - - @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .errorMessage(TestConstants.BROKEN_MESSAGE) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - - @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .errorMessage(TestConstants.BROKEN_MESSAGE) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .errorMessage(TestConstants.BROKEN_MESSAGE) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - - @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .errorMessage(TestConstants.BROKEN_MESSAGE) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - - @Override - public ProviderEvaluation getObjectEvaluation( - String key, Value defaultValue, EvaluationContext invocationContext) { - return ProviderEvaluation.builder() - .errorMessage(TestConstants.BROKEN_MESSAGE) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } -} 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/BooleanHookTest.java b/src/test/java/dev/openfeature/sdk/BooleanHookTest.java new file mode 100644 index 000000000..a38f3ff79 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/BooleanHookTest.java @@ -0,0 +1,31 @@ +package dev.openfeature.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BooleanHookTest implements HookFixtures { + + private Hook hook; + + @BeforeEach + void setupTest() { + hook = mockBooleanHook(); + } + + @Test + void verifyFlagValueTypeIsSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.BOOLEAN); + + assertThat(hookSupported).isTrue(); + } + + @Test + void verifyFlagValueTypeIsNotSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.INTEGER); + + assertThat(hookSupported).isFalse(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java index cd7e8b295..6bbb2e6c3 100644 --- a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java +++ b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java @@ -2,22 +2,28 @@ import static org.junit.jupiter.api.Assertions.*; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import dev.openfeature.sdk.testutils.testProvider.TestProvider; 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()); + var provider1 = TestProvider.builder().initsToReady(); + var provider2 = TestProvider.builder().initsToReady(); + + api.setProviderAndWait("client1", provider1); + api.setProviderAndWait("client2", provider2); Client c1 = api.getClient("client1"); Client c2 = api.getClient("client2"); - assertTrue(c1.getBooleanValue("test", false)); - assertFalse(c2.getBooleanValue("test", false)); + c1.getBooleanValue("test", false); + c2.getBooleanValue("test", false); + + assertEquals(1, provider1.getFlagEvaluations().size()); + assertEquals(1, provider2.getFlagEvaluations().size()); } } diff --git a/src/test/java/dev/openfeature/sdk/DefaultHookDataTest.java b/src/test/java/dev/openfeature/sdk/DefaultHookDataTest.java new file mode 100644 index 000000000..ac50988ea --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/DefaultHookDataTest.java @@ -0,0 +1,81 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DefaultHookDataTest { + + @Test + void shouldStoreAndRetrieveValues() { + var hookData = new DefaultHookData(); + + hookData.set("key1", "value1"); + hookData.set("key2", 42); + hookData.set("key3", true); + + assertEquals("value1", hookData.get("key1")); + assertEquals(42, hookData.get("key2")); + assertEquals(true, hookData.get("key3")); + } + + @Test + void shouldReturnNullForMissingKeys() { + var hookData = new DefaultHookData(); + + assertNull(hookData.get("nonexistent")); + } + + @Test + void shouldSupportTypeSafeRetrieval() { + var hookData = new DefaultHookData(); + + hookData.set("string", "hello"); + hookData.set("integer", 123); + hookData.set("boolean", false); + + assertEquals("hello", hookData.get("string", String.class)); + assertEquals(Integer.valueOf(123), hookData.get("integer", Integer.class)); + assertEquals(Boolean.FALSE, hookData.get("boolean", Boolean.class)); + } + + @Test + void shouldReturnNullForMissingKeysWithType() { + var hookData = new DefaultHookData(); + + assertNull(hookData.get("missing", String.class)); + } + + @Test + void shouldThrowClassCastExceptionForWrongType() { + var hookData = new DefaultHookData(); + + hookData.set("string", "not a number"); + + assertThrows(ClassCastException.class, () -> { + hookData.get("string", Integer.class); + }); + } + + @Test + void shouldOverwriteExistingValues() { + var hookData = new DefaultHookData(); + + hookData.set("key", "original"); + assertEquals("original", hookData.get("key")); + + hookData.set("key", "updated"); + assertEquals("updated", hookData.get("key")); + } + + @Test + void shouldSupportNullValues() { + var hookData = new DefaultHookData(); + + hookData.set("nullKey", null); + assertNull(hookData.get("nullKey")); + assertNull(hookData.get("nullKey", String.class)); + } +} diff --git a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java index aacf09169..19108bde5 100644 --- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java +++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java @@ -8,23 +8,28 @@ 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 dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.Arrays; import java.util.HashMap; import java.util.List; 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 + void setUp() { + api = new OpenFeatureAPI(); + } @Test void simpleBooleanFlag() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new TestEventsProvider()); + api.setProviderAndWait(TestProvider.builder().initsToReady()); Client client = api.getClient(); Boolean retval = client.getBooleanValue(flagKey, false); assertFalse(retval); @@ -34,8 +39,7 @@ void simpleBooleanFlag() { void clientHooks() { Hook exampleHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new TestEventsProvider()); + api.setProviderAndWait(TestProvider.builder().initsToReady()); Client client = api.getClient(); client.addHooks(exampleHook); Boolean retval = client.getBooleanValue(flagKey, false); @@ -48,8 +52,7 @@ void evalHooks() { Hook clientHook = mockBooleanHook(); Hook evalHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new TestEventsProvider()); + api.setProviderAndWait(TestProvider.builder().initsToReady()); Client client = api.getClient(); client.addHooks(clientHook); Boolean retval = client.getBooleanValue( @@ -69,8 +72,7 @@ void evalHooks() { @Test void providingContext() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new TestEventsProvider()); + api.setProviderAndWait(TestProvider.builder().initsToReady()); Client client = api.getClient(); Map attributes = new HashMap<>(); List values = Arrays.asList(new Value(2), new Value(4)); @@ -86,8 +88,7 @@ void providingContext() { @Test void brokenProvider() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation()); Client client = api.getClient(); FlagEvaluationDetails retval = client.getBooleanDetails(flagKey, false); assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode()); @@ -99,6 +100,11 @@ void brokenProvider() { @Test void providerLockedPerTransaction() { + final String defaultValue = "string-value"; + final OpenFeatureAPI api = new OpenFeatureAPI(); + var provider1 = TestProvider.builder().initsToReady(); + var provider2 = TestProvider.builder().initsToReady(); + class MutatingHook implements Hook { @Override @@ -106,34 +112,31 @@ 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(provider2); return Optional.empty(); } } - final String defaultValue = "string-value"; - final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(provider1); api.addHooks(new MutatingHook()); // if provider is changed during an evaluation transaction it should proceed with the original provider - String doSomethingValue = client.getStringValue("val", defaultValue); - assertEquals(new StringBuilder(defaultValue).reverse().toString(), doSomethingValue); + client.getStringValue("val", defaultValue); + assertEquals(1, provider1.getFlagEvaluations().size()); api.clearHooks(); // subsequent evaluations should now use new provider set by hook - String noOpValue = client.getStringValue("val", defaultValue); - assertEquals(noOpValue, defaultValue); + client.getStringValue("val", defaultValue); + assertEquals(1, provider2.getFlagEvaluations().size()); } @Test void setProviderAndWaitShouldPutTheProviderInReadyState() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(domain, new TestEventsProvider()); + api.setProviderAndWait(domain, TestProvider.builder().initsToReady()); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); } @@ -145,12 +148,11 @@ void setProviderAndWaitShouldPutTheProviderInReadyState() { @Test void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - TestEventsProvider provider = new TestEventsProvider(); + var provider = TestProvider.builder().initsToReady(); 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 +163,11 @@ void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { @Test void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - TestEventsProvider provider = new TestEventsProvider(); + var provider = TestProvider.builder().initsToReady(); 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 +178,13 @@ void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { @Test void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - TestEventsProvider provider = new TestEventsProvider(); + var provider = TestProvider.builder().initsToReady(); 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/DoSomethingProvider.java b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java deleted file mode 100644 index 0477a725b..000000000 --- a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -package dev.openfeature.sdk; - -class DoSomethingProvider implements FeatureProvider { - - static final String name = "Something"; - // Flag evaluation metadata - static final ImmutableMetadata DEFAULT_METADATA = - ImmutableMetadata.builder().build(); - private ImmutableMetadata flagMetadata; - - public DoSomethingProvider() { - this.flagMetadata = DEFAULT_METADATA; - } - - public DoSomethingProvider(ImmutableMetadata flagMetadata) { - this.flagMetadata = flagMetadata; - } - - @Override - public Metadata getMetadata() { - return () -> name; - } - - @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(!defaultValue) - .flagMetadata(flagMetadata) - .build(); - } - - @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(new StringBuilder(defaultValue).reverse().toString()) - .flagMetadata(flagMetadata) - .build(); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue * 100) - .flagMetadata(flagMetadata) - .build(); - } - - @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue * 100) - .flagMetadata(flagMetadata) - .build(); - } - - @Override - public ProviderEvaluation getObjectEvaluation( - String key, Value defaultValue, EvaluationContext invocationContext) { - return ProviderEvaluation.builder() - .value(null) - .flagMetadata(flagMetadata) - .build(); - } -} diff --git a/src/test/java/dev/openfeature/sdk/DoubleHookTest.java b/src/test/java/dev/openfeature/sdk/DoubleHookTest.java new file mode 100644 index 000000000..9b198ee83 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/DoubleHookTest.java @@ -0,0 +1,31 @@ +package dev.openfeature.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DoubleHookTest implements HookFixtures { + + private Hook hook; + + @BeforeEach + void setupTest() { + hook = mockDoubleHook(); + } + + @Test + void verifyFlagValueTypeIsSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.DOUBLE); + + assertThat(hookSupported).isTrue(); + } + + @Test + void verifyFlagValueTypeIsNotSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.STRING); + + assertThat(hookSupported).isFalse(); + } +} 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..b3cd2a05d 100644 --- a/src/test/java/dev/openfeature/sdk/EventsTest.java +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -4,14 +4,19 @@ import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; - -import dev.openfeature.sdk.testutils.TestEventsProvider; -import io.cucumber.java.AfterAll; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import dev.openfeature.sdk.testutils.testProvider.TestProvider; 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 +24,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 @@ -48,9 +54,10 @@ void apiInitReady() { final Consumer handler = mockHandler(); final String name = "apiInitReady"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().onProviderReady(handler); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + var provider = + TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + api.onProviderReady(handler); + api.setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()).accept(any()); } @@ -63,14 +70,15 @@ void apiInitReady() { void apiInitError() { final Consumer handler = mockHandler(); final String name = "apiInitError"; - final String errMessage = "oh no!"; - - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - OpenFeatureAPI.getInstance().onProviderError(handler); - OpenFeatureAPI.getInstance().setProvider(name, provider); - verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { - return errMessage.equals(details.getMessage()); - })); + + var provider = TestProvider.builder() + .withName(name) + .initWaitsFor(INIT_DELAY) + .initsToError(); + api.onProviderError(handler); + api.setProvider(name, provider); + verify(handler, timeout(TIMEOUT)) + .accept(argThat(details -> name.equals(details.getProviderName()))); } } @@ -88,11 +96,12 @@ void apiShouldPropagateEvents() { final Consumer handler = mockHandler(); final String name = "apiShouldPropagateEvents"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); + var provider = + TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + api.setProviderAndWait(name, provider); + api.onProviderConfigurationChanged(handler); - provider.mockEvent( + provider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)).accept(any()); @@ -117,17 +126,17 @@ void apiShouldSupportAllEventTypes() { final Consumer handler3 = mockHandler(); final Consumer handler4 = mockHandler(); - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + var provider = + TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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( - eventType, ProviderEventDetails.builder().build()); + provider.emit(eventType, ProviderEventDetails.builder().build()); }); verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any()); @@ -160,13 +169,14 @@ class ProviderEvents { void shouldPropagateDefaultAndAnon() { final Consumer handler = mockHandler(); - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); + var provider = + TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); // 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( + provider.emit( ProviderEvent.PROVIDER_STALE, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -181,13 +191,14 @@ void shouldPropagateDefaultAndNamed() { final Consumer handler = mockHandler(); final String name = "shouldPropagateDefaultAndNamed"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); + var provider = + TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); // 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( + provider.emit( ProviderEvent.PROVIDER_STALE, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -212,11 +223,11 @@ void initReadyProviderBefore() { final Consumer handler = mockHandler(); final String name = "initReadyProviderBefore"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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))); } @@ -231,10 +242,10 @@ void initReadyProviderAfter() { final Consumer handler = mockHandler(); final String name = "initReadyProviderAfter"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); // 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))); @@ -249,16 +260,13 @@ void initReadyProviderAfter() { void initErrorProviderAfter() { final Consumer handler = mockHandler(); final String name = "initErrorProviderAfter"; - final String errMessage = "oh no!"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToError(); + Client client = api.getClient(name); client.onProviderError(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); - verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { - return name.equals(details.getDomain()) && errMessage.equals(details.getMessage()); - })); + api.setProvider(name, provider); + verify(handler, timeout(TIMEOUT)).accept(argThat(details -> name.equals(details.getDomain()))); } @Test @@ -270,15 +278,14 @@ void initErrorProviderAfter() { void initErrorProviderBefore() { final Consumer handler = mockHandler(); final String name = "initErrorProviderBefore"; - final String errMessage = "oh no!"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToError(); // 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()); + return name.equals(details.getDomain()); })); } } @@ -297,13 +304,13 @@ void shouldPropagateBefore() { final Consumer handler = mockHandler(); final String name = "shouldPropagateBefore"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); // 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( + provider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)) @@ -321,13 +328,13 @@ void shouldPropagateAfter() { final Consumer handler = mockHandler(); final String name = "shouldPropagateAfter"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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( + provider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)) @@ -353,9 +360,9 @@ void shouldSupportAllEventTypes() { final Consumer handler3 = mockHandler(); final Consumer handler4 = mockHandler(); - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderReady(handler1); client.onProviderConfigurationChanged(handler2); @@ -363,7 +370,7 @@ void shouldSupportAllEventTypes() { client.onProviderError(handler4); Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { - provider.mockEvent(eventType, ProviderEventDetails.builder().build()); + provider.emit(eventType, ProviderEventDetails.builder().build()); }); ArgumentMatcher nameMatches = (EventDetails details) -> details.getDomain().equals(name); @@ -382,24 +389,29 @@ void shouldNotRunHandlers() { final Consumer handler2 = mockHandler(); final String name = "shouldNotRunHandlers"; - TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); - TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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()); + await().until(provider1::isShutdown); // fire old event - provider1.mockEvent( - ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, - EventDetails.builder().build()); + try { + provider1.emit( + ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, + EventDetails.builder().build()); + } catch (Exception e) { + // ignore this exception. When the provider is shutdown, so is the underlying ExecutorService. If new tasks + // are scheduled, they will be rejected and an exception will be thrown. + } // a bit of waiting here, but we want to make sure these are indeed never // called. @@ -419,18 +431,19 @@ void otherClientHandlersShouldNotRun() { final Consumer handlerToRun = mockHandler(); final Consumer handlerNotToRun = mockHandler(); - TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); - TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name1, provider1); - OpenFeatureAPI.getInstance().setProviderAndWait(name2, provider2); + var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + + 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); - provider1.mockEvent( + provider1.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); @@ -448,24 +461,24 @@ void boundShouldNotRunWithDefault() { final String name = "boundShouldNotRunWithDefault"; final Consumer handlerNotToRun = mockHandler(); - TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY); - TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + var namedProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + var defaultProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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)); + await().until(() -> ProviderState.READY.equals(client.getProviderState())); // fire event on default provider - defaultProvider.mockEvent( + defaultProvider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(handlerNotToRun, after(TIMEOUT).never()).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -478,22 +491,22 @@ void unboundShouldRunWithDefault() { final String name = "unboundShouldRunWithDefault"; final Consumer handlerToRun = mockHandler(); - TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + var defaultProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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 - await().until(() -> defaultProvider.getState().equals(ProviderState.READY)); + await().until(() -> ProviderState.READY.equals(client.getProviderState())); // fire event on default provider - defaultProvider.mockEvent( + defaultProvider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(handlerToRun, timeout(TIMEOUT)).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -508,16 +521,16 @@ void handlersRunIfOneThrows() { final Consumer nextHandler = mockHandler(); final Consumer lastHandler = mockHandler(); - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + api.setProviderAndWait(name, provider); - Client client1 = OpenFeatureAPI.getInstance().getClient(name); + Client client1 = api.getClient(name); client1.onProviderConfigurationChanged(errorHandler); client1.onProviderConfigurationChanged(nextHandler); client1.onProviderConfigurationChanged(lastHandler); - provider.mockEvent( + provider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(errorHandler, timeout(TIMEOUT)).accept(any()); @@ -536,12 +549,12 @@ void shouldHaveAllProperties() { final Consumer handler2 = mockHandler(); final String name = "shouldHaveAllProperties"; - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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"); @@ -554,7 +567,7 @@ void shouldHaveAllProperties() { .message(message) .build(); - provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); + provider.emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details); // both global and client handler should have all the fields. verify(handler1, timeout(TIMEOUT)).accept(argThat((EventDetails eventDetails) -> { @@ -577,15 +590,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); + var provider = TestProvider.builder().initsToReady(); + 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 +609,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(); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); 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 +630,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(); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); 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()); } @@ -641,14 +653,14 @@ void mustPersistAcrossChanges() { final String name = "mustPersistAcrossChanges"; final Consumer handler = mockHandler(); - TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); - TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); + var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); - 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( + provider1.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); ArgumentMatcher nameMatches = @@ -657,11 +669,11 @@ 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. - provider2.mockEvent( + provider2.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(handler, timeout(TIMEOUT).times(2)).accept(argThat(nameMatches)); @@ -680,19 +692,19 @@ void removedEventsShouldNotRun() { final Consumer handler1 = mockHandler(); final Consumer handler2 = mockHandler(); - TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady(); + 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 - provider.mockEvent( + provider.emit( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index 2ad88d328..82aa4e3cc 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -1,16 +1,24 @@ package dev.openfeature.sdk; -import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import dev.openfeature.sdk.e2e.Flag; 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 dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,26 +37,21 @@ class FlagEvaluationSpecTest implements HookFixtures { private OpenFeatureAPI api; private Client _client() { - FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); return api.getClient(); } @SneakyThrows private Client _initializedClient() { - TestEventsProvider provider = new TestEventsProvider(); + var provider = TestProvider.builder().initsToReady(); 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 +65,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 +72,7 @@ void global_singleton() { @Test void provider() { FeatureProvider mockProvider = mock(FeatureProvider.class); - FeatureProviderTestUtils.setFeatureProvider(mockProvider); + api.setProviderAndWait(mockProvider); assertThat(api.getProvider()).isEqualTo(mockProvider); } @@ -89,14 +83,14 @@ void provider() { "The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.") @Test void providerAndWait() { - FeatureProvider provider = new TestEventsProvider(500); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var provider = TestProvider.builder().initWaitsFor(500).initsToReady(); + api.setProviderAndWait(provider); Client client = api.getClient(); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); - provider = new TestEventsProvider(500); + provider = TestProvider.builder().initWaitsFor(500).initsToReady(); String providerName = "providerAndWait"; - OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider); + api.setProviderAndWait(providerName, provider); Client client2 = api.getClient(providerName); assertThat(client2.getProviderState()).isEqualTo(ProviderState.READY); } @@ -108,10 +102,10 @@ void providerAndWait() { "The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.") @Test void providerAndWaitError() { - FeatureProvider provider1 = new TestEventsProvider(500, true, "fake error"); + var provider1 = TestProvider.builder().initWaitsFor(500).initsToError(); assertThrows(GeneralError.class, () -> api.setProviderAndWait(provider1)); - FeatureProvider provider2 = new TestEventsProvider(500, true, "fake error"); + var provider2 = TestProvider.builder().initWaitsFor(500).initsToError(); String providerName = "providerAndWaitError"; assertThrows(GeneralError.class, () -> api.setProviderAndWait(providerName, provider2)); } @@ -122,10 +116,10 @@ void providerAndWaitError() { "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.") @Test void shouldReturnNotReadyIfNotInitialized() { - FeatureProvider provider = new TestEventsProvider(100); + var provider = TestProvider.builder().initWaitsFor(500).initsToReady(); 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,8 +130,9 @@ 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()); - assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name); + var name = "name"; + api.setProviderAndWait(TestProvider.builder().withName(name).initsToReady()); + assertThat(api.getProviderMetadata().getName()).isEqualTo(name); } @Specification( @@ -198,57 +193,63 @@ 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(TestProvider.builder() + .withFlags( + new Flag(FlagValueType.BOOLEAN.name(), "boolean", true), + new Flag(FlagValueType.STRING.name(), "string", "default"), + new Flag(FlagValueType.INTEGER.name(), "int", 400), + new Flag(FlagValueType.DOUBLE.name(), "double", 40.0), + new Flag(FlagValueType.OBJECT.name(), "obj", new Value())) + .initsToReady()); Client c = api.getClient(); - String key = "key"; - assertEquals(true, c.getBooleanValue(key, false)); - assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext())); + assertEquals(true, c.getBooleanValue("boolean", false)); + assertEquals(true, c.getBooleanValue("boolean", false, new ImmutableContext())); assertEquals( true, c.getBooleanValue( - key, + "boolean", false, new ImmutableContext(), FlagEvaluationOptions.builder().build())); - assertEquals("gnirts-ym", c.getStringValue(key, "my-string")); - assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext())); + assertEquals("default", c.getStringValue("string", "my-string")); + assertEquals("default", c.getStringValue("string", "my-string", new ImmutableContext())); assertEquals( - "gnirts-ym", + "default", c.getStringValue( - key, + "string", "my-string", new ImmutableContext(), FlagEvaluationOptions.builder().build())); - assertEquals(400, c.getIntegerValue(key, 4)); - assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext())); + assertEquals(400, c.getIntegerValue("int", 3)); + assertEquals(400, c.getIntegerValue("int", 3, new ImmutableContext())); assertEquals( 400, c.getIntegerValue( - key, + "int", 4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); - assertEquals(40.0, c.getDoubleValue(key, .4)); - assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext())); + assertEquals(40.0, c.getDoubleValue("double", .4)); + assertEquals(40.0, c.getDoubleValue("double", .4, new ImmutableContext())); assertEquals( 40.0, c.getDoubleValue( - key, + "double", .4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); - assertEquals(null, c.getObjectValue(key, new Value())); - assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext())); + assertEquals(new Value(), c.getObjectValue("obj", new Value())); + assertEquals(new Value(), c.getObjectValue("obj", new Value(), new ImmutableContext())); assertEquals( - null, + new Value(), c.getObjectValue( - key, + "obj", new Value(), new ImmutableContext(), FlagEvaluationOptions.builder().build())); @@ -279,68 +280,80 @@ 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(TestProvider.builder() + .withFlags( + new Flag(FlagValueType.BOOLEAN.name(), "boolean", true), + new Flag(FlagValueType.STRING.name(), "string", "default"), + new Flag(FlagValueType.INTEGER.name(), "int", 400), + new Flag(FlagValueType.DOUBLE.name(), "double", 40.0), + new Flag(FlagValueType.OBJECT.name(), "obj", new Value())) + .initsToReady()); Client c = api.getClient(); - String key = "key"; FlagEvaluationDetails bd = FlagEvaluationDetails.builder() - .flagKey(key) - .value(false) - .variant(null) - .flagMetadata(DEFAULT_METADATA) + .flagKey("boolean") + .value(true) + .variant(TestProvider.DEFAULT_VARIANT) + .flagMetadata(ImmutableMetadata.EMPTY) + .reason(Reason.STATIC.name()) .build(); - assertEquals(bd, c.getBooleanDetails(key, true)); - assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext())); + assertEquals(bd, c.getBooleanDetails("boolean", false)); + assertEquals(bd, c.getBooleanDetails("boolean", false, new ImmutableContext())); assertEquals( bd, c.getBooleanDetails( - key, - true, + "boolean", + false, new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails sd = FlagEvaluationDetails.builder() - .flagKey(key) - .value("tset") - .variant(null) - .flagMetadata(DEFAULT_METADATA) + .flagKey("string") + .value("default") + .variant(TestProvider.DEFAULT_VARIANT) + .flagMetadata(ImmutableMetadata.EMPTY) + .reason(Reason.STATIC.name()) .build(); - assertEquals(sd, c.getStringDetails(key, "test")); - assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext())); + assertEquals(sd, c.getStringDetails("string", "test")); + assertEquals(sd, c.getStringDetails("string", "test", new ImmutableContext())); assertEquals( sd, c.getStringDetails( - key, + "string", "test", new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails id = FlagEvaluationDetails.builder() - .flagKey(key) + .flagKey("int") .value(400) - .flagMetadata(DEFAULT_METADATA) + .flagMetadata(ImmutableMetadata.EMPTY) + .reason(Reason.STATIC.name()) + .variant(TestProvider.DEFAULT_VARIANT) .build(); - assertEquals(id, c.getIntegerDetails(key, 4)); - assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext())); + assertEquals(id, c.getIntegerDetails("int", 4)); + assertEquals(id, c.getIntegerDetails("int", 4, new ImmutableContext())); assertEquals( id, c.getIntegerDetails( - key, + "int", 4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails dd = FlagEvaluationDetails.builder() - .flagKey(key) + .flagKey("double") .value(40.0) - .flagMetadata(DEFAULT_METADATA) + .flagMetadata(ImmutableMetadata.EMPTY) + .reason(Reason.STATIC.name()) + .variant(TestProvider.DEFAULT_VARIANT) .build(); - assertEquals(dd, c.getDoubleDetails(key, .4)); - assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext())); + assertEquals(dd, c.getDoubleDetails("double", .4)); + assertEquals(dd, c.getDoubleDetails("double", .4, new ImmutableContext())); assertEquals( dd, c.getDoubleDetails( - key, + "double", .4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); @@ -386,7 +399,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -414,8 +427,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -431,7 +444,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); @@ -450,7 +463,7 @@ void clientMetadata() { assertNull(c.getMetadata().getDomain()); String domainName = "test domain"; - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c2 = api.getClient(domainName); assertEquals(domainName, c2.getMetadata().getName()); @@ -463,7 +476,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertEquals(Reason.ERROR.toString(), result.getReason()); @@ -475,7 +488,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(TestProvider.builder().allowUnknownFlags().initsToReady()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertNotNull(result.getFlagMetadata()); @@ -486,8 +499,8 @@ void flag_metadata_passed() { void api_context() { String contextKey = "some-key"; String contextValue = "some-value"; - DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + var provider = spy(TestProvider.builder().allowUnknownFlags().initsToReady()); + api.setProviderAndWait(provider); Map attributes = new HashMap<>(); attributes.put(contextKey, new Value(contextValue)); @@ -513,8 +526,8 @@ void api_context() { "Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.") @Test void multi_layer_context_merges_correctly() { - DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + var provider = spy(TestProvider.builder().allowUnknownFlags().initsToReady()); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); Hook hook = spy(new Hook() { @@ -701,8 +714,8 @@ public void after( text = "The API SHOULD have a method for setting a transaction context propagator.") @Test void setting_transaction_context_propagator() { - DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + var provider = spy(TestProvider.builder().initsToReady()); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); @@ -715,8 +728,8 @@ void setting_transaction_context_propagator() { "The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.") @Test void setting_transaction_context() { - DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + var provider = spy(TestProvider.builder().initsToReady()); + 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/HookContextTest.java b/src/test/java/dev/openfeature/sdk/HookContextTest.java index 2196b8b1f..a37ade9d5 100644 --- a/src/test/java/dev/openfeature/sdk/HookContextTest.java +++ b/src/test/java/dev/openfeature/sdk/HookContextTest.java @@ -1,6 +1,6 @@ package dev.openfeature.sdk; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import org.junit.jupiter.api.Test; diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index d6247c649..163007120 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -6,6 +6,7 @@ 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 static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; @@ -18,8 +19,7 @@ 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 dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,16 +28,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 +165,7 @@ void optional_properties() { .type(FlagValueType.INTEGER) .ctx(new ImmutableContext()) .defaultValue(1) - .clientMetadata(OpenFeatureAPI.getInstance().getClient().getMetadata()) + .clientMetadata(api.getClient().getMetadata()) .build(); } @@ -173,8 +175,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client client = api.getClient(); Hook evalHook = mockBooleanHook(); @@ -216,8 +218,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,40 +260,42 @@ 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() { - - @Override - public Optional before(HookContext ctx, Map hints) { - evalOrder.add("provider before"); - return null; - } - - @Override - public void after( - HookContext ctx, - FlagEvaluationDetails details, - Map hints) { - evalOrder.add("provider after"); - } - - @Override - public void error(HookContext ctx, Exception error, Map hints) { - evalOrder.add("provider error"); - } - - @Override - public void finallyAfter( - HookContext ctx, - FlagEvaluationDetails details, - Map hints) { - evalOrder.add("provider finally"); - } - }); - } - }); + + api.setProviderAndWait( + "evalOrder", + TestProvider.builder() + .withHook(new BooleanHook() { + + @Override + public Optional before( + HookContext ctx, Map hints) { + evalOrder.add("provider before"); + return null; + } + + @Override + public void after( + HookContext ctx, + FlagEvaluationDetails details, + Map hints) { + evalOrder.add("provider after"); + } + + @Override + public void error(HookContext ctx, Exception error, Map hints) { + evalOrder.add("provider error"); + } + + @Override + public void finallyAfter( + HookContext ctx, + FlagEvaluationDetails details, + Map hints) { + evalOrder.add("provider finally"); + } + }) + .allowUnknownFlags() + .initsToReady()); api.addHooks(new BooleanHook() { @Override public Optional before(HookContext ctx, Map hints) { @@ -411,8 +414,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(TestProvider.builder().withExceptionOnFlagEvaluation()); Client c = api.getClient(); c.getBooleanDetails( @@ -435,7 +437,7 @@ void error_stops_after() { doThrow(RuntimeException.class).when(h).after(any(), any(), any()); Hook h2 = mockBooleanHook(); - Client c = getClient(TestEventsProvider.newInitializedTestEventsProvider()); + Client c = getClient(TestProvider.builder().allowUnknownFlags().initsToReady()); c.getBooleanDetails( "key", @@ -516,8 +518,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", @@ -541,7 +542,7 @@ void flag_eval_hook_order() { void error_hooks__before() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).before(any(), any()); - Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); + Client client = getClient(TestProvider.builder().initsToReady()); Boolean value = client.getBooleanValue( "key", false, @@ -559,7 +560,7 @@ void error_hooks__before() { void error_hooks__after() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).after(any(), any(), any()); - Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); + Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady()); client.getBooleanValue( "key", false, @@ -574,7 +575,7 @@ void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).after(any(), any(), any()); String flagKey = "test-flag-key"; - Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); + Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady()); client.getBooleanValue( flagKey, true, @@ -596,11 +597,30 @@ void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() { assertThat(evaluationDetails.getValue()).isTrue(); } + @Test + void shortCircuit_flagResolution_runsHooksWithAllFields() { + String domain = "shortCircuit_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails"; + api.setProvider(domain, TestProvider.builder().initsToFatal()); + + 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(); String flagKey = "test-flag-key"; - Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); + Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady()); client.getBooleanValue( flagKey, true, @@ -613,7 +633,7 @@ void successful_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() { FlagEvaluationDetails evaluationDetails = captor.getValue(); assertThat(evaluationDetails).isNotNull(); assertThat(evaluationDetails.getErrorCode()).isNull(); - assertThat(evaluationDetails.getReason()).isEqualTo("DEFAULT"); + assertThat(evaluationDetails.getReason()).isEqualTo(Reason.STATIC.name()); assertThat(evaluationDetails.getVariant()).isEqualTo("Passed in default"); assertThat(evaluationDetails.getFlagKey()).isEqualTo(flagKey); assertThat(evaluationDetails.getFlagMetadata()) @@ -695,8 +715,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 +780,10 @@ void first_error_broken() { } private Client getClient(FeatureProvider provider) { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); if (provider == null) { - FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + api.setProviderAndWait(TestProvider.builder().initsToReady()); } else { - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); } return api.getClient(); } @@ -786,4 +804,28 @@ void doesnt_use_finally() { Hook.class.getMethod("finallyAfter", HookContext.class, FlagEvaluationDetails.class, Map.class)) .doesNotThrowAnyException(); } + + @Specification( + number = "4.6.1", + text = "hook data MUST be a structure supporting the definition of arbitrary " + + "properties, with keys of type string, and values of any type.") + @Test + void hook_data_structure() { + // Arrange + HookData hookData = new DefaultHookData(); + + // Act - Add arbitrary properties to hook data + hookData.set("stringKey", "StringValue"); // String value + hookData.set("intKey", 42); // Integer value + hookData.set("doubleKey", 3.14); // Double value + hookData.set("objectKey", new Object()); // Object value + hookData.set("nullKey", null); // Null value + + // Assert - Retrieve and validate the properties + assertEquals("StringValue", hookData.get("stringKey")); + assertEquals(42, hookData.get("intKey")); + assertEquals(3.14, hookData.get("doubleKey")); + assertNotNull(hookData.get("objectKey")); + assertNull(hookData.get("nullKey")); + } } diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index 02a8ff90c..b1bb70ba1 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -2,13 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import dev.openfeature.sdk.fixtures.HookFixtures; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -17,22 +18,30 @@ import org.junit.jupiter.params.provider.EnumSource; class HookSupportTest implements HookFixtures { + + private static final HookSupport hookSupport = new HookSupport(); + @Test @DisplayName("should merge EvaluationContexts on before hooks correctly") void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() { Map attributes = new HashMap<>(); attributes.put("baseKey", new Value("baseValue")); - EvaluationContext baseContext = new ImmutableContext(attributes); - HookContext hookContext = new HookContext<>( - "flagKey", FlagValueType.STRING, "defaultValue", baseContext, () -> "client", () -> "provider"); + EvaluationContext baseEvalContext = new ImmutableContext(attributes); + Hook hook1 = mockStringHook(); Hook hook2 = mockStringHook(); when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber"))); when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar"))); - HookSupport hookSupport = new HookSupport(); - EvaluationContext result = hookSupport.beforeHooks( - FlagValueType.STRING, hookContext, Arrays.asList(hook1, hook2), Collections.emptyMap()); + var sharedContext = getBaseHookContextForType(FlagValueType.STRING); + var hookSupportData = new HookSupportData(); + hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING); + hookSupport.setHookContexts(hookSupportData, sharedContext); + hookSupport.updateEvaluationContext(hookSupportData, baseEvalContext); + + hookSupport.executeBeforeHooks(hookSupportData); + + EvaluationContext result = hookSupportData.getEvaluationContext(); assertThat(result.getValue("bla").asString()).isEqualTo("blubber"); assertThat(result.getValue("foo").asString()).isEqualTo("bar"); @@ -44,37 +53,11 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() { @DisplayName("should always call generic hook") void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { Hook genericHook = mockGenericHook(); - HookSupport hookSupport = new HookSupport(); - EvaluationContext baseContext = new ImmutableContext(); - IllegalStateException expectedException = new IllegalStateException("All fine, just a test"); - HookContext hookContext = new HookContext<>( - "flagKey", - flagValueType, - createDefaultValue(flagValueType), - baseContext, - () -> "client", - () -> "provider"); - - hookSupport.beforeHooks( - flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap()); - hookSupport.afterHooks( - flagValueType, - hookContext, - FlagEvaluationDetails.builder().build(), - Collections.singletonList(genericHook), - Collections.emptyMap()); - hookSupport.afterAllHooks( - flagValueType, - hookContext, - FlagEvaluationDetails.builder().build(), - Collections.singletonList(genericHook), - Collections.emptyMap()); - hookSupport.errorHooks( - flagValueType, - hookContext, - expectedException, - Collections.singletonList(genericHook), - Collections.emptyMap()); + + var hookSupportData = new HookSupportData(); + hookSupport.setHooks(hookSupportData, List.of(genericHook), flagValueType); + + callAllHooks(hookSupportData); verify(genericHook).before(any(), any()); verify(genericHook).after(any(), any(), any()); @@ -82,6 +65,82 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { verify(genericHook).error(any(), any(), any()); } + @ParameterizedTest + @EnumSource(value = FlagValueType.class) + @DisplayName("should allow hooks to store and retrieve data across stages") + void shouldPassDataAcrossStages(FlagValueType flagValueType) { + var testHook = new TestHookWithData(); + var hookSupportData = new HookSupportData(); + hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType); + hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType)); + + hookSupport.executeBeforeHooks(hookSupportData); + assertHookData(testHook, "before"); + + hookSupport.executeAfterHooks( + hookSupportData, FlagEvaluationDetails.builder().build()); + assertHookData(testHook, "before", "after"); + + hookSupport.executeAfterAllHooks( + hookSupportData, FlagEvaluationDetails.builder().build()); + assertHookData(testHook, "before", "after", "finallyAfter"); + + hookSupport.executeErrorHooks(hookSupportData, mock(Exception.class)); + assertHookData(testHook, "before", "after", "finallyAfter", "error"); + } + + @ParameterizedTest + @EnumSource(value = FlagValueType.class) + @DisplayName("should isolate data between different hook instances") + void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { + var testHook1 = new TestHookWithData(1); + var testHook2 = new TestHookWithData(2); + + var hookSupportData = new HookSupportData(); + hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType); + hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType)); + + callAllHooks(hookSupportData); + + assertHookData(testHook1, 1, "before", "after", "finallyAfter", "error"); + assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error"); + } + + private static void callAllHooks(HookSupportData hookSupportData) { + hookSupport.executeBeforeHooks(hookSupportData); + hookSupport.executeAfterHooks( + hookSupportData, FlagEvaluationDetails.builder().build()); + hookSupport.executeAfterAllHooks( + hookSupportData, FlagEvaluationDetails.builder().build()); + hookSupport.executeErrorHooks(hookSupportData, mock(Exception.class)); + } + + private static void assertHookData(TestHookWithData testHook, String... expectedKeys) { + for (String expectedKey : expectedKeys) { + assertThat(testHook.hookData.get(expectedKey)) + .withFailMessage("Expected key %s not present in hook data", expectedKey) + .isNotNull(); + } + } + + private static void assertHookData(TestHookWithData testHook, Object expectedValue, String... expectedKeys) { + for (String expectedKey : expectedKeys) { + assertThat(testHook.hookData.get(expectedKey)) + .withFailMessage("Expected key '%s' not present in hook data", expectedKey) + .isNotNull(); + assertThat(testHook.hookData.get(expectedKey)) + .withFailMessage( + "Expected key '%s' not containing expected value. Expected '%s' but found '%s'", + expectedKey, expectedValue, testHook.hookData.get(expectedKey)) + .isEqualTo(expectedValue); + } + } + + private SharedHookContext getBaseHookContextForType(FlagValueType flagValueType) { + return new SharedHookContext<>( + "flagKey", flagValueType, () -> "client", () -> "provider", createDefaultValue(flagValueType)); + } + private Object createDefaultValue(FlagValueType flagValueType) { switch (flagValueType) { case INTEGER: @@ -102,7 +161,6 @@ private Object createDefaultValue(FlagValueType flagValueType) { private EvaluationContext evaluationContextWithValue(String key, String value) { Map attributes = new HashMap<>(); attributes.put(key, new Value(value)); - EvaluationContext baseContext = new ImmutableContext(attributes); - return baseContext; + return new ImmutableContext(attributes); } } diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java index e69a974b3..0b8a44d0d 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,44 @@ void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() { Structure value = key1.asStructure(); assertArrayEquals(new Object[] {"key1_1"}, value.keySet().toArray()); } + + @DisplayName("Merge should obtain keys from the overriding context when the existing context is empty") + @Test + void mergeShouldObtainKeysFromOverridingContextWhenExistingContextIsEmpty() { + HashMap attributes = new HashMap<>(); + attributes.put("key1", new Value("val1")); + attributes.put("key2", new Value("val2")); + + EvaluationContext ctx = new ImmutableContext(); + EvaluationContext overriding = new ImmutableContext(attributes); + EvaluationContext merge = ctx.merge(overriding); + assertEquals(new java.util.HashSet<>(java.util.Arrays.asList("key1", "key2")), merge.keySet()); + } + + @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..108fac0fe --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/ImmutableMetadataTest.java @@ -0,0 +1,41 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.Map; +import org.junit.jupiter.api.Assertions; +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); + } + + @Test + void retrieveAsUnmodifiableMap() { + ImmutableMetadata metadata = + ImmutableMetadata.builder().addString("key1", "value1").build(); + + Map unmodifiableMap = metadata.asUnmodifiableMap(); + assertEquals(unmodifiableMap.size(), 1); + assertEquals(unmodifiableMap.get("key1"), "value1"); + Assertions.assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.put("key3", "value3")); + } +} 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/IntegerHookTest.java b/src/test/java/dev/openfeature/sdk/IntegerHookTest.java new file mode 100644 index 000000000..1dee4bd8f --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/IntegerHookTest.java @@ -0,0 +1,31 @@ +package dev.openfeature.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IntegerHookTest implements HookFixtures { + + private Hook hook; + + @BeforeEach + void setupTest() { + hook = mockIntegerHook(); + } + + @Test + void verifyFlagValueTypeIsSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.INTEGER); + + assertThat(hookSupported).isTrue(); + } + + @Test + void verifyFlagValueTypeIsNotSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.STRING); + + assertThat(hookSupported).isFalse(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java similarity index 77% rename from src/test/java/dev/openfeature/sdk/LockingTest.java rename to src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java index 4b7af5530..ae3246cae 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java @@ -15,12 +15,11 @@ import org.junit.jupiter.api.parallel.Isolated; @Isolated() -class LockingTest { +class LockingSingeltonTest { private static OpenFeatureAPI api; private OpenFeatureClient client; private AutoCloseableReentrantReadWriteLock apiLock; - private AutoCloseableReentrantReadWriteLock clientContextLock; private AutoCloseableReentrantReadWriteLock clientHooksLock; @BeforeAll @@ -36,10 +35,7 @@ void beforeEach() { apiLock = setupLock(apiLock, mockInnerReadLock(), mockInnerWriteLock()); OpenFeatureAPI.lock = apiLock; - clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock()); clientHooksLock = setupLock(clientHooksLock, mockInnerReadLock(), mockInnerWriteLock()); - client.contextLock = clientContextLock; - client.hooksLock = clientHooksLock; } @Nested @@ -137,50 +133,6 @@ void onProviderErrorProviderReadyShouldApiWriteLockAndUnlock() { } } - @Test - void addHooksShouldWriteLockAndUnlock() { - client.addHooks(new Hook() {}); - verify(clientHooksLock.writeLock()).lock(); - verify(clientHooksLock.writeLock()).unlock(); - - api.addHooks(new Hook() {}); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - - @Test - void getHooksShouldReadLockAndUnlock() { - client.getHooks(); - verify(clientHooksLock.readLock()).lock(); - verify(clientHooksLock.readLock()).unlock(); - - api.getHooks(); - verify(apiLock.readLock()).lock(); - verify(apiLock.readLock()).unlock(); - } - - @Test - void setContextShouldWriteLockAndUnlock() { - client.setEvaluationContext(new ImmutableContext()); - verify(clientContextLock.writeLock()).lock(); - verify(clientContextLock.writeLock()).unlock(); - - api.setEvaluationContext(new ImmutableContext()); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - - @Test - void getContextShouldReadLockAndUnlock() { - client.getEvaluationContext(); - verify(clientContextLock.readLock()).lock(); - verify(clientContextLock.readLock()).unlock(); - - api.getEvaluationContext(); - verify(apiLock.readLock()).lock(); - verify(apiLock.readLock()).unlock(); - } - @Test void setTransactionalContextPropagatorShouldWriteLockAndUnlock() { api.setTransactionContextPropagator(new NoOpTransactionContextPropagator()); @@ -195,13 +147,6 @@ void getTransactionalContextPropagatorShouldReadLockAndUnlock() { verify(apiLock.readLock()).unlock(); } - @Test - void clearHooksShouldWriteLockAndUnlock() { - api.clearHooks(); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - private static ReentrantReadWriteLock.ReadLock mockInnerReadLock() { ReentrantReadWriteLock.ReadLock readLockMock = mock(ReentrantReadWriteLock.ReadLock.class); doNothing().when(readLockMock).lock(); 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/ObjectHookTest.java b/src/test/java/dev/openfeature/sdk/ObjectHookTest.java new file mode 100644 index 000000000..7e474c0bd --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/ObjectHookTest.java @@ -0,0 +1,31 @@ +package dev.openfeature.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ObjectHookTest implements HookFixtures { + + private Hook hook; + + @BeforeEach + void setupTest() { + hook = mockObjectHook(); + } + + @Test + void verifyFlagValueTypeIsSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.OBJECT); + + assertThat(hookSupported).isTrue(); + } + + @Test + void verifyFlagValueTypeIsNotSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.INTEGER); + + assertThat(hookSupported).isFalse(); + } +} 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..2fdb4e3f0 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -5,12 +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 dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.HashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,13 +21,13 @@ class OpenFeatureAPITest { @BeforeEach void setupTest() { - api = OpenFeatureAPI.getInstance(); + api = new OpenFeatureAPI(); } @Test void namedProviderTest() { - FeatureProvider provider = new NoOpProvider(); - FeatureProviderTestUtils.setFeatureProvider("namedProviderTest", provider); + var provider = TestProvider.builder().initsToReady(); + api.setProviderAndWait("namedProviderTest", provider); assertThat(provider.getMetadata().getName()) .isEqualTo(api.getProviderMetadata("namedProviderTest").getName()); @@ -42,35 +40,32 @@ void namedProviderTest() { @Test 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); + var provider1 = TestProvider.builder().withName("provider1").initsToReady(); + var provider2 = TestProvider.builder().withName("provider2").initsToReady(); + api.setProviderAndWait(domain, provider1); + api.setProviderAndWait(domain, provider2); + + assertThat(api.getProvider(domain).getMetadata().getName()) + .isEqualTo(provider2.getMetadata().getName()); } @Test void providerToMultipleNames() throws Exception { - FeatureProvider inMemAsEventingProvider = new InMemoryProvider(Collections.EMPTY_MAP); - FeatureProvider noOpAsNonEventingProvider = new NoOpProvider(); + var inMemAsEventingProvider = TestProvider.builder().initsToReady(); + var noOpAsNonEventingProvider = TestProvider.builder().initsToReady(); // 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 @@ -99,28 +94,25 @@ void setEvaluationContextShouldAllowChaining() { @Test void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception { String domain = "namedProviderOverwrittenTest"; - FeatureProvider provider1 = new NoOpProvider(); - FeatureProvider provider2 = new TestEventsProvider(); - FeatureProviderTestUtils.setFeatureProvider(domain, provider1); - FeatureProviderTestUtils.setFeatureProvider(domain, provider2); + var provider1 = TestProvider.builder().initsToReady(); + var provider2 = TestProvider.builder().initsToReady(); + 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..91509bd45 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -10,12 +10,14 @@ import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.TestEventsProvider; +import dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.HashMap; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.simplify4u.slf4jmock.LoggerMock; import org.slf4j.Logger; @@ -38,12 +40,13 @@ 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()); + "shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", + TestProvider.builder().initsToReady()); Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext"); client.addHooks(mockBooleanHook(), mockStringHook()); - FlagEvaluationDetails actual = client.getBooleanDetails("feature key", Boolean.FALSE); + FlagEvaluationDetails actual = client.getBooleanDetails("feature key", Boolean.TRUE); assertThat(actual.getValue()).isTrue(); // I dislike this, but given the mocking tools available, there's no way that I know of to say "no errors were @@ -81,8 +84,8 @@ void setEvaluationContextShouldAllowChaining() { @Test @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(); + var provider = TestProvider.builder().initWaitsFor(100).initsToFatal(); + OpenFeatureAPI api = new OpenFeatureAPI(); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState"); assertThrows( @@ -96,12 +99,40 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { @Test @DisplayName("Should not call evaluation methods when the provider has state NOT_READY") void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() { - FeatureProvider provider = new TestEventsProvider(5000); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + var awaitable = new Awaitable(); + var provider = TestProvider.builder().initWaitsFor(awaitable).initsToReady(); + OpenFeatureAPI api = new OpenFeatureAPI(); api.setProvider("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState", provider); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState"); FlagEvaluationDetails details = client.getBooleanDetails("key", true); assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_NOT_READY); + awaitable.wakeup(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("Should support usage of HookData with/without error") + void shouldSupportUsageOfHookData(boolean isError) { + OpenFeatureAPI api = new OpenFeatureAPI(); + api.setProviderAndWait( + "shouldSupportUsageOfHookData", + TestProvider.builder().allowUnknownFlags(!isError).initsToReady()); + + var testHook = new TestHookWithData("test-data"); + api.addHooks(testHook); + + Client client = api.getClient("shouldSupportUsageOfHookData"); + client.getBooleanDetails("key", true); + + assertThat(testHook.hookData.get("before")).isEqualTo("test-data"); + assertThat(testHook.hookData.get("finallyAfter")).isEqualTo("test-data"); + if (isError) { + assertThat(testHook.hookData.get("after")).isEqualTo(null); + assertThat(testHook.hookData.get("error")).isEqualTo("test-data"); + } else { + assertThat(testHook.hookData.get("after")).isEqualTo("test-data"); + assertThat(testHook.hookData.get("error")).isEqualTo(null); + } } } 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/StringHookTest.java b/src/test/java/dev/openfeature/sdk/StringHookTest.java new file mode 100644 index 000000000..16c0cd41b --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/StringHookTest.java @@ -0,0 +1,31 @@ +package dev.openfeature.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.fixtures.HookFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringHookTest implements HookFixtures { + + private Hook hook; + + @BeforeEach + void setupTest() { + hook = mockStringHook(); + } + + @Test + void verifyFlagValueTypeIsSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.STRING); + + assertThat(hookSupported).isTrue(); + } + + @Test + void verifyFlagValueTypeIsNotSupportedByHook() { + boolean hookSupported = hook.supportsFlagValueType(FlagValueType.INTEGER); + + assertThat(hookSupported).isFalse(); + } +} 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/TestHookWithData.java b/src/test/java/dev/openfeature/sdk/TestHookWithData.java new file mode 100644 index 000000000..dc415fa16 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/TestHookWithData.java @@ -0,0 +1,42 @@ +package dev.openfeature.sdk; + +import java.util.Map; +import java.util.Optional; + +class TestHookWithData implements Hook { + private final Object value; + HookData hookData = null; + + public TestHookWithData(Object value) { + this.value = value; + } + + public TestHookWithData() { + this("test"); + } + + @Override + public Optional before(HookContext ctx, Map hints) { + ctx.getHookData().set("before", value); + hookData = ctx.getHookData(); + return Optional.empty(); + } + + @Override + public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { + ctx.getHookData().set("after", value); + hookData = ctx.getHookData(); + } + + @Override + public void error(HookContext ctx, Exception error, Map hints) { + ctx.getHookData().set("error", value); + hookData = ctx.getHookData(); + } + + @Override + public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) { + ctx.getHookData().set("finallyAfter", value); + hookData = ctx.getHookData(); + } +} 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/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index 5bc89d03d..d6a03efd6 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -6,14 +6,18 @@ import static dev.openfeature.sdk.testutils.TestFlagsUtils.OBJECT_FLAG_KEY; import static dev.openfeature.sdk.testutils.TestFlagsUtils.STRING_FLAG_KEY; +import dev.openfeature.sdk.BooleanHook; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.DoubleHook; import dev.openfeature.sdk.EvaluationContext; -import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.HookContext; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.IntegerHook; import dev.openfeature.sdk.NoOpProvider; +import dev.openfeature.sdk.ObjectHook; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.StringHook; import dev.openfeature.sdk.Value; import java.util.HashMap; import java.util.Map; @@ -25,7 +29,7 @@ /** * Runs a large volume of flag evaluations on a VM with 1G memory and GC - * completely disabled so we can take a heap-dump. + * completely disabled, so we can take a heap-dump. */ public class AllocationBenchmark { @@ -48,12 +52,36 @@ public void run() { Map clientAttrs = new HashMap<>(); clientAttrs.put("client", new Value(2)); client.setEvaluationContext(new ImmutableContext(clientAttrs)); - client.addHooks(new Hook() { + client.addHooks(new ObjectHook() { @Override public Optional before(HookContext ctx, Map hints) { return Optional.ofNullable(new ImmutableContext()); } }); + client.addHooks(new StringHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(new ImmutableContext()); + } + }); + client.addHooks(new BooleanHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(new ImmutableContext()); + } + }); + client.addHooks(new IntegerHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(new ImmutableContext()); + } + }); + client.addHooks(new DoubleHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(new ImmutableContext()); + } + }); Map invocationAttrs = new HashMap<>(); invocationAttrs.put("invoke", new Value(3)); 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/Flag.java b/src/test/java/dev/openfeature/sdk/e2e/Flag.java new file mode 100644 index 000000000..7e3a11c90 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/Flag.java @@ -0,0 +1,21 @@ +package dev.openfeature.sdk.e2e; + +import dev.openfeature.sdk.ImmutableMetadata; + +public class Flag { + public final String name; + public final Object defaultValue; + public final String type; + public final ImmutableMetadata flagMetadata; + + public Flag(String type, String name, Object defaultValue) { + this(type, name, defaultValue, ImmutableMetadata.EMPTY); + } + + public Flag(String type, String name, Object defaultValue, ImmutableMetadata flagMetadata) { + this.name = name; + this.defaultValue = defaultValue; + this.type = type; + this.flagMetadata = flagMetadata; + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java b/src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java similarity index 50% rename from src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java rename to src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java index 8a3381412..89c7161be 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java +++ b/src/test/java/dev/openfeature/sdk/e2e/GherkinSpecTest.java @@ -1,16 +1,20 @@ 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.ExcludeTags; 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") -public class EvaluationTest {} +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.steps") +@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") +@ExcludeTags({"deprecated", "reason-codes-cached", "async", "immutability", "evaluation-options"}) +public class GherkinSpecTest {} 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..565968c1c --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/Utils.java @@ -0,0 +1,39 @@ +package dev.openfeature.sdk.e2e; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfeature.sdk.Value; +import java.util.Objects; + +public final class Utils { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + 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); + case "object": + try { + return Value.objectToValue(OBJECT_MAPPER.readValue(value, Object.class)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + 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..ce9bb8b5f --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ContextSteps.java @@ -0,0 +1,132 @@ +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.ImmutableStructure; +import dev.openfeature.sdk.MutableContext; +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 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 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; + } + } + } + + @Given("a context containing a key {string} with null value") + public void a_context_containing_a_key_with_null_value(String key) { + a_context_containing_a_key_with_type_and_with_value(key, "String", null); + } + + @Given("a context containing a key {string}, with type {string} and with value {string}") + public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value) { + Map map = state.context.asMap(); + map.put(key, Value.objectToValue(Utils.convert(value, type))); + state.context = new MutableContext(state.context.getTargetingKey(), map); + } + + @Given("a context containing a targeting key with value {string}") + public void a_context_containing_a_targeting_key_with_value(String string) { + state.context.setTargetingKey(string); + } + + @Given("a context containing a nested property with outer key {string} and inner key {string}, with value {string}") + public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value( + String outer, String inner, String value) { + Map innerMap = new HashMap<>(); + innerMap.put(inner, new Value(value)); + state.context.add(outer, new ImmutableStructure(innerMap)); + } +} 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..dccdbf9af --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/FlagStepDefinitions.java @@ -0,0 +1,132 @@ +package dev.openfeature.sdk.e2e.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.openfeature.sdk.ErrorCode; +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 fallback 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) { + Object evaluationValue = state.evaluation.getValue(); + if (state.flag.type.equalsIgnoreCase("object")) { + assertThat(((Value) evaluationValue).asStructure().asObjectMap()) + .isEqualTo(((Value) Utils.convert(value, state.flag.type)) + .asStructure() + .asObjectMap()); + } else { + assertThat(evaluationValue).isEqualTo(Utils.convert(value, state.flag.type)); + } + } + + @Then("the flag key should be {string}") + public void the_flag_key_should_be(String key) { + assertThat(state.evaluation.getFlagKey()).isEqualTo(key); + } + + @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 error-code should be {string}") + public void the_error_code_should_be(String errorCode) { + if (errorCode.isEmpty()) { + assertThat(state.evaluation.getErrorCode()).isNull(); + } else { + assertThat(state.evaluation.getErrorCode()).isEqualTo(ErrorCode.valueOf(errorCode)); + } + } + + @Then("the error message should contain {string}") + public void the_error_message_should_contain(String messageSubstring) { + assertThat(state.evaluation.getErrorMessage()).contains(messageSubstring); + } + + @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..f22a0811a --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/e2e/steps/ProviderSteps.java @@ -0,0 +1,157 @@ +package dev.openfeature.sdk.e2e.steps; + +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.ProviderState; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.e2e.State; +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import java.util.Map; + +public class ProviderSteps { + private final State state; + + public ProviderSteps(State state) { + this.state = state; + } + + @Given("a {} provider") + public void a_provider_with_status(String providerType) throws Exception { + // Normalize input to handle both single word and quoted strings + String normalizedType = + providerType.toLowerCase().replaceAll("[\"\\s]+", " ").trim(); + + switch (normalizedType) { + case "not ready": + setupMockProvider(ErrorCode.PROVIDER_NOT_READY, "Provider in not ready state", ProviderState.NOT_READY); + break; + case "stable": + case "ready": + setupStableProvider(); + break; + case "fatal": + setupMockProvider(ErrorCode.PROVIDER_FATAL, "Provider in fatal state", ProviderState.FATAL); + break; + case "error": + setupMockProvider(ErrorCode.GENERAL, "Provider in error state", ProviderState.ERROR); + break; + case "stale": + setupMockProvider(null, null, ProviderState.STALE); + break; + default: + throw new IllegalArgumentException("Unsupported provider type: " + providerType); + } + } + + // =============================== + // Provider Status Assertion Steps + // =============================== + + @Then("the provider status should be {string}") + public void the_provider_status_should_be(String expectedStatus) { + ProviderState actualStatus = state.client.getProviderState(); + ProviderState expected = ProviderState.valueOf(expectedStatus); + assertThat(actualStatus).isEqualTo(expected); + } + + // =============================== + // Helper Methods + // =============================== + + private void setupStableProvider() throws Exception { + Map> flags = buildFlags(); + InMemoryProvider provider = new InMemoryProvider(flags); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + state.client = OpenFeatureAPI.getInstance().getClient(); + } + + private void setupMockProvider(ErrorCode errorCode, String errorMessage, ProviderState providerState) + throws Exception { + EventProvider mockProvider = spy(EventProvider.class); + + switch (providerState) { + case NOT_READY: + doAnswer(invocationOnMock -> { + while (true) {} + }) + .when(mockProvider) + .initialize(any()); + break; + case FATAL: + doThrow(new FatalError(errorMessage)).when(mockProvider).initialize(any()); + break; + } + // Configure all evaluation methods with a single helper + configureMockEvaluations(mockProvider, errorCode, errorMessage); + + OpenFeatureAPI.getInstance().setProvider(providerState.name(), mockProvider); + Client client = OpenFeatureAPI.getInstance().getClient(providerState.name()); + state.client = client; + + ProviderEventDetails details = + ProviderEventDetails.builder().errorCode(errorCode).build(); + switch (providerState) { + case FATAL: + case ERROR: + mockProvider.emitProviderReady(details).await(); + mockProvider.emitProviderError(details).await(); + break; + case STALE: + mockProvider.emitProviderReady(details).await(); + mockProvider.emitProviderStale(details).await(); + break; + default: + } + } + + private void configureMockEvaluations(FeatureProvider mockProvider, ErrorCode errorCode, String errorMessage) { + // Configure Boolean evaluation + when(mockProvider.getBooleanEvaluation(anyString(), any(Boolean.class), any())) + .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage)); + + // Configure String evaluation + when(mockProvider.getStringEvaluation(anyString(), any(String.class), any())) + .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage)); + + // Configure Integer evaluation + when(mockProvider.getIntegerEvaluation(anyString(), any(Integer.class), any())) + .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage)); + + // Configure Double evaluation + when(mockProvider.getDoubleEvaluation(anyString(), any(Double.class), any())) + .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage)); + + // Configure Object evaluation + when(mockProvider.getObjectEvaluation(anyString(), any(Value.class), any())) + .thenAnswer(invocation -> createProviderEvaluation(invocation.getArgument(1), errorCode, errorMessage)); + } + + private ProviderEvaluation createProviderEvaluation( + T defaultValue, ErrorCode errorCode, String errorMessage) { + return ProviderEvaluation.builder() + .value(defaultValue) + .errorCode(errorCode) + .errorMessage(errorMessage) + .reason(Reason.ERROR.toString()) + .build(); + } +} 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..c31e9eb7e 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; @@ -22,6 +21,7 @@ import java.util.Map; import lombok.SneakyThrows; +@Deprecated public class StepDefinitions { private static Client client; @@ -289,7 +289,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 +309,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/fixtures/HookFixtures.java b/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java index b94e58a11..d2d51bac7 100644 --- a/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java +++ b/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java @@ -6,6 +6,7 @@ import dev.openfeature.sdk.DoubleHook; import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.IntegerHook; +import dev.openfeature.sdk.ObjectHook; import dev.openfeature.sdk.StringHook; public interface HookFixtures { @@ -26,6 +27,10 @@ default Hook mockDoubleHook() { return spy(DoubleHook.class); } + default Hook mockObjectHook() { + return spy(ObjectHook.class); + } + default Hook mockGenericHook() { return spy(Hook.class); } diff --git a/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java new file mode 100644 index 000000000..0de360ae6 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/internal/ConfigurableThreadFactoryTest.java @@ -0,0 +1,42 @@ +package dev.openfeature.sdk.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ConfigurableThreadFactoryTest { + + private static final String THREAD_NAME = "testthread"; + private final Runnable runnable = () -> {}; + + @Test + void verifyNewThreadHasNamePrefix() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME); + var thread = configurableThreadFactory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(thread.isDaemon()).isFalse(); + } + + @Test + void verifyNewThreadHasNamePrefixWithIncrement() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME); + var threadOne = configurableThreadFactory.newThread(runnable); + var threadTwo = configurableThreadFactory.newThread(runnable); + + assertThat(threadOne.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(threadTwo.getName()).isEqualTo(THREAD_NAME + "-2"); + } + + @Test + void verifyNewDaemonThreadHasNamePrefix() { + + var configurableThreadFactory = new ConfigurableThreadFactory(THREAD_NAME, true); + var thread = configurableThreadFactory.newThread(runnable); + + assertThat(thread.getName()).isEqualTo(THREAD_NAME + "-1"); + assertThat(thread.isDaemon()).isTrue(); + } +} 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/TestEventsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java deleted file mode 100644 index 7cd2ea318..000000000 --- a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java +++ /dev/null @@ -1,127 +0,0 @@ -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.Reason; -import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.FatalError; -import dev.openfeature.sdk.exceptions.GeneralError; -import lombok.SneakyThrows; - -public class TestEventsProvider extends EventProvider { - public static final String PASSED_IN_DEFAULT = "Passed in default"; - - private boolean initError = false; - private String initErrorMessage; - private boolean shutDown = false; - private int initTimeoutMs = 0; - private String name = "test"; - private Metadata metadata = () -> name; - private boolean isFatalInitError = false; - - public TestEventsProvider() {} - - public TestEventsProvider(int initTimeoutMs) { - this.initTimeoutMs = initTimeoutMs; - } - - public TestEventsProvider(int initTimeoutMs, boolean initError, String initErrorMessage) { - this.initTimeoutMs = initTimeoutMs; - this.initError = initError; - this.initErrorMessage = initErrorMessage; - } - - public TestEventsProvider(int initTimeoutMs, boolean initError, String initErrorMessage, boolean fatal) { - this.initTimeoutMs = initTimeoutMs; - this.initError = initError; - this.initErrorMessage = initErrorMessage; - this.isFatalInitError = fatal; - } - - @SneakyThrows - public static TestEventsProvider newInitializedTestEventsProvider() { - TestEventsProvider provider = new TestEventsProvider(); - provider.initialize(null); - return provider; - } - - public void mockEvent(ProviderEvent event, ProviderEventDetails details) { - emit(event, details); - } - - public boolean isShutDown() { - return this.shutDown; - } - - @Override - public void shutdown() { - this.shutDown = true; - } - - @Override - public void initialize(EvaluationContext evaluationContext) throws Exception { - // wait half the TIMEOUT, otherwise some init/errors can be fired before we add handlers - Thread.sleep(initTimeoutMs); - if (this.initError) { - if (this.isFatalInitError) { - throw new FatalError(initErrorMessage); - } - throw new GeneralError(initErrorMessage); - } - } - - @Override - public Metadata getMetadata() { - return this.metadata; - } - - @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(PASSED_IN_DEFAULT) - .reason(Reason.DEFAULT.toString()) - .build(); - } - - @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(PASSED_IN_DEFAULT) - .reason(Reason.DEFAULT.toString()) - .build(); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(PASSED_IN_DEFAULT) - .reason(Reason.DEFAULT.toString()) - .build(); - } - - @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(PASSED_IN_DEFAULT) - .reason(Reason.DEFAULT.toString()) - .build(); - } - - @Override - public ProviderEvaluation getObjectEvaluation( - String key, Value defaultValue, EvaluationContext invocationContext) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(PASSED_IN_DEFAULT) - .reason(Reason.DEFAULT.toString()) - .build(); - } -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java index 157b07175..7c45166f9 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -1,17 +1,28 @@ package dev.openfeature.sdk.testutils; -import static dev.openfeature.sdk.Structure.mapToStructure; +import static dev.openfeature.sdk.e2e.Utils.OBJECT_MAPPER; -import com.google.common.collect.ImmutableMap; -import dev.openfeature.sdk.Value; +import com.fasterxml.jackson.core.StreamReadFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.providers.memory.ContextEvaluator; import dev.openfeature.sdk.providers.memory.Flag; -import java.util.HashMap; +import dev.openfeature.sdk.testutils.jackson.ContextEvaluatorDeserializer; +import dev.openfeature.sdk.testutils.jackson.ImmutableMetadataDeserializer; +import dev.openfeature.sdk.testutils.jackson.InMemoryFlagMixin; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Collections; import java.util.Map; import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; /** * Test flags utils. */ +@Slf4j @UtilityClass public class TestFlagsUtils { @@ -22,74 +33,39 @@ 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"; + private static Map> flags; /** * Building flags for testing purposes. + * * @return map of flags */ - public static Map> buildFlags() { - Map> flags = new HashMap<>(); - flags.put( - BOOLEAN_FLAG_KEY, - Flag.builder() - .variant("on", true) - .variant("off", false) - .defaultVariant("on") - .build()); - flags.put( - STRING_FLAG_KEY, - Flag.builder() - .variant("greeting", "hi") - .variant("parting", "bye") - .defaultVariant("greeting") - .build()); - flags.put( - INT_FLAG_KEY, - Flag.builder() - .variant("one", 1) - .variant("ten", 10) - .defaultVariant("ten") - .build()); - flags.put( - FLOAT_FLAG_KEY, - Flag.builder() - .variant("tenth", 0.1) - .variant("half", 0.5) - .defaultVariant("half") - .build()); - flags.put( - OBJECT_FLAG_KEY, - Flag.builder() - .variant("empty", new HashMap<>()) - .variant( - "template", - new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100))))) - .defaultVariant("template") - .build()); - flags.put( - CONTEXT_AWARE_FLAG_KEY, - Flag.builder() - .variant("internal", "INTERNAL") - .variant("external", "EXTERNAL") - .defaultVariant("external") - .contextEvaluator((flag, evaluationContext) -> { - if (new Value(false).equals(evaluationContext.getValue("customer"))) { - return (String) flag.getVariants().get("internal"); - } else { - return (String) flag.getVariants().get(flag.getDefaultVariant()); - } - }) - .build()); - flags.put( - WRONG_FLAG_KEY, - Flag.builder() - .variant("one", "uno") - .variant("two", "dos") - .defaultVariant("one") - .build()); + public static synchronized Map> buildFlags() { + if (flags == null) { + ObjectMapper objectMapper = OBJECT_MAPPER; + objectMapper.configure(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION.mappedFeature(), true); + objectMapper.addMixIn(Flag.class, InMemoryFlagMixin.class); + objectMapper.addMixIn(Flag.FlagBuilder.class, InMemoryFlagMixin.FlagBuilderMixin.class); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(ImmutableMetadata.class, new ImmutableMetadataDeserializer()); + module.addDeserializer(ContextEvaluator.class, new ContextEvaluatorDeserializer()); + objectMapper.registerModule(module); + + Map> flagsJson; + try { + flagsJson = objectMapper.readValue( + Paths.get("spec/specification/assets/gherkin/test-flags.json") + .toFile(), + new TypeReference<>() {}); + + } catch (IOException e) { + throw new RuntimeException(e); + } + flags = Collections.unmodifiableMap(flagsJson); + } + 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/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java new file mode 100644 index 000000000..6ca3875ef --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/CelContextEvaluator.java @@ -0,0 +1,61 @@ +package dev.openfeature.sdk.testutils.jackson; + +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.providers.memory.ContextEvaluator; +import dev.openfeature.sdk.providers.memory.Flag; +import java.util.HashMap; +import java.util.Map; + +public class CelContextEvaluator implements ContextEvaluator { + private final CelRuntime.Program program; + + public CelContextEvaluator(String expression) { + try { + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() + .addVar("customer", SimpleType.BOOL) + .addVar("email", SimpleType.STRING) + .addVar("age", SimpleType.INT) + .addVar("dummy", SimpleType.STRING) + .setResultType(SimpleType.STRING) + // Add other variables you expect + .build(); + + var ast = celCompiler.compile(expression).getAst(); + this.program = celRuntime.createProgram(ast); + } catch (Exception e) { + throw new RuntimeException("Failed to compile CEL expression: " + expression, e); + } + } + + @Override + @SuppressWarnings("unchecked") + public T evaluate(Flag flag, EvaluationContext evaluationContext) { + try { + Map objectMap = new HashMap<>(); + // Provide defaults for all declared variables to prevent runtime errors. + objectMap.put("email", ""); + objectMap.put("customer", true); + objectMap.put("age", 0); + objectMap.put("dummy", ""); + + if (evaluationContext != null) { + // Evaluate with context, overriding defaults. + objectMap.putAll(evaluationContext.asObjectMap()); + } + + Object result = program.eval(objectMap); + + String stringResult = (String) result; + return (T) flag.getVariants().get(stringResult); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java new file mode 100644 index 000000000..e348fc8c5 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/ContextEvaluatorDeserializer.java @@ -0,0 +1,25 @@ +package dev.openfeature.sdk.testutils.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import dev.openfeature.sdk.providers.memory.ContextEvaluator; +import java.io.IOException; + +public class ContextEvaluatorDeserializer extends JsonDeserializer> { + @Override + public ContextEvaluator deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + + if (node.isTextual()) { + return new CelContextEvaluator<>(node.asText()); + } + + if (node.isObject() && node.has("expression")) { + return new CelContextEvaluator<>(node.get("expression").asText()); + } + + return null; + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java new file mode 100644 index 000000000..09f7c6f24 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/ImmutableMetadataDeserializer.java @@ -0,0 +1,41 @@ +package dev.openfeature.sdk.testutils.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import dev.openfeature.sdk.ImmutableMetadata; +import java.io.IOException; +import java.util.Map; + +public class ImmutableMetadataDeserializer extends JsonDeserializer { + @Override + public ImmutableMetadata deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Map properties = p.readValueAs(new TypeReference>() {}); + + ImmutableMetadata.ImmutableMetadataBuilder builder = ImmutableMetadata.builder(); + + if (properties != null) { + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + builder.addString(key, (String) value); + } else if (value instanceof Integer) { + builder.addInteger(key, (Integer) value); + } else if (value instanceof Long) { + builder.addLong(key, (Long) value); + } else if (value instanceof Float) { + builder.addFloat(key, (Float) value); + } else if (value instanceof Double) { + builder.addDouble(key, (Double) value); + } else if (value instanceof Boolean) { + builder.addBoolean(key, (Boolean) value); + } + } + } + + return builder.build(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/InMemoryFlagMixin.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/InMemoryFlagMixin.java new file mode 100644 index 000000000..dd0154cdd --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/InMemoryFlagMixin.java @@ -0,0 +1,20 @@ +package dev.openfeature.sdk.testutils.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import dev.openfeature.sdk.providers.memory.Flag; +import java.util.Map; + +@JsonDeserialize(builder = Flag.FlagBuilder.class) +@SuppressWarnings("rawtypes") +public abstract class InMemoryFlagMixin { + + @JsonPOJOBuilder(withPrefix = "") + public abstract class FlagBuilderMixin { + + @JsonProperty("variants") + @JsonDeserialize(using = VariantsMapDeserializer.class) + public abstract Flag.FlagBuilder variants(Map variants); + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/jackson/VariantsMapDeserializer.java b/src/test/java/dev/openfeature/sdk/testutils/jackson/VariantsMapDeserializer.java new file mode 100644 index 000000000..f7a621cbb --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/jackson/VariantsMapDeserializer.java @@ -0,0 +1,65 @@ +package dev.openfeature.sdk.testutils.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import dev.openfeature.sdk.Value; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class VariantsMapDeserializer extends JsonDeserializer> { + + @Override + public Map deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + Map variants = new HashMap<>(); + + Iterator> fields = node.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + String variantKey = field.getKey(); + JsonNode variantNode = field.getValue(); + + // Convert the variant value to OpenFeature Value + Object variantValue = convertToValue(p, variantNode); + variants.put(variantKey, variantValue); + } + + return variants; + } + + private Object convertToValue(JsonParser p, JsonNode node) throws JsonProcessingException { + // If the node has a "value" property, use that + if (node.isObject() && node.has("value")) { + return convertJsonNodeToValue(p, node.get("value")); + } + + // Otherwise, treat the entire node as the value + return convertJsonNodeToValue(p, node); + } + + private Object convertJsonNodeToValue(JsonParser p, JsonNode node) throws JsonProcessingException { + if (node.isNull()) { + return null; + } else if (node.isBoolean()) { + return node.asBoolean(); + } else if (node.isInt()) { + return node.asInt(); + } else if (node.isDouble()) { + return node.asDouble(); + } else if (node.isTextual()) { + return node.asText(); + } else if (node.isArray()) { + return Value.objectToValue(p.getCodec().treeToValue(node, List.class)); + } else if (node.isObject()) { + return Value.objectToValue(p.getCodec().treeToValue(node, Object.class)); + } + + throw new IllegalArgumentException("Unsupported JSON node type: " + node.getNodeType()); + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java b/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java new file mode 100644 index 000000000..61860a050 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java @@ -0,0 +1,16 @@ +package dev.openfeature.sdk.testutils.testProvider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FlagValueType; + +public class FlagEvaluation { + public final String flagKey; + public final FlagValueType flagType; + public final EvaluationContext evaluationContext; + + public FlagEvaluation(String flagKey, FlagValueType flagType, EvaluationContext evaluationContext) { + this.flagKey = flagKey; + this.flagType = flagType; + this.evaluationContext = evaluationContext; + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java b/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java new file mode 100644 index 000000000..383ade483 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java @@ -0,0 +1,396 @@ +package dev.openfeature.sdk.testutils.testProvider; + +import dev.openfeature.sdk.*; +import dev.openfeature.sdk.e2e.Flag; +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TestProvider extends EventProvider { + public static final String DEFAULT_VARIANT = "Passed in default"; + + private final String name; + private final List hooks; + private final int initDelay; + private final Awaitable initWaitsFor; + private final Map flags; + private final boolean fatalOnInit; + private final boolean errorOnInit; + private final boolean errorsOnFlagEvaluation; + private final ErrorCode errorCode; + private final String errorMessage; + private final RuntimeException throwable; + private final ConcurrentLinkedQueue flagEvaluations = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final boolean allowAnyFlag; + + private TestProvider( + String name, + List hooks, + int initDelay, + Awaitable initWaitsFor, + boolean allowAnyFlag, + Map flags, + boolean errorsOnFlagEvaluation, + boolean errorOnInit, + ErrorCode errorCode, + String errorMessage, + RuntimeException throwable, + boolean fatalOnInit) { + this.name = name == null ? "TestProvider" : name; + this.hooks = hooks; + this.initDelay = initDelay; + this.initWaitsFor = initWaitsFor; + this.allowAnyFlag = allowAnyFlag; + this.flags = flags; + this.errorsOnFlagEvaluation = errorsOnFlagEvaluation; + this.errorOnInit = errorOnInit; + this.errorCode = errorCode == null ? ErrorCode.GENERAL : errorCode; + this.errorMessage = errorMessage == null ? "Test error" : errorMessage; + this.throwable = throwable; + this.fatalOnInit = fatalOnInit; + } + + public List getFlagEvaluations() { + return new ArrayList<>(flagEvaluations); + } + + @Override + public List getProviderHooks() { + return hooks; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + if (initWaitsFor != null) { + initWaitsFor.await(); + } else if (initDelay > 0) { + var end = System.currentTimeMillis() + initDelay; + long delta = initDelay; + while (delta > 0) { + try { + Thread.sleep(delta); + } catch (InterruptedException e) { + // ignore + } + delta = end - System.currentTimeMillis(); + } + } + + if (fatalOnInit) { + throw new FatalError("TestProvider is set to fatal state, thus will throw on init"); + } + if (errorOnInit) { + throw new RuntimeException("TestProvider is set to error state, thus will throw on init"); + } + } + + @Override + public void shutdown() { + super.shutdown(); + isShutdown.set(true); + } + + public boolean isShutdown() { + return isShutdown.get(); + } + + @Override + public Metadata getMetadata() { + return () -> name; + } + + private ProviderEvaluation getEvaluation( + String key, T defaultValue, FlagValueType flagType, Class clazz, EvaluationContext evaluationContext) { + flagEvaluations.add(new FlagEvaluation(key, flagType, evaluationContext)); + if (throwable != null) { + throw throwable; + } + var builder = ProviderEvaluation.builder(); + if (errorsOnFlagEvaluation) { + return builder.errorMessage(errorMessage).errorCode(errorCode).build(); + } + if (allowAnyFlag) { + return builder.reason(Reason.STATIC.name()) + .value(clazz.cast(defaultValue)) + .flagMetadata(ImmutableMetadata.EMPTY) + .variant(DEFAULT_VARIANT) + .build(); + } + var flag = flags.get(key); + if (flag == null) { + return builder.errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag not found") + .build(); + } + if (flagType.name().equals(flag.type)) { + return builder.reason(Reason.STATIC.name()) + .value(clazz.cast(flag.defaultValue)) + .flagMetadata(flag.flagMetadata) + .variant(DEFAULT_VARIANT) + .build(); + } + return builder.errorCode(ErrorCode.TYPE_MISMATCH) + .errorMessage("Flag type mismatch") + .flagMetadata(flag.flagMetadata) + .build(); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return getEvaluation(key, defaultValue, FlagValueType.BOOLEAN, Boolean.class, ctx); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return getEvaluation(key, defaultValue, FlagValueType.STRING, String.class, ctx); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return getEvaluation(key, defaultValue, FlagValueType.INTEGER, Integer.class, ctx); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return getEvaluation(key, defaultValue, FlagValueType.DOUBLE, Double.class, ctx); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return getEvaluation(key, defaultValue, FlagValueType.OBJECT, Value.class, ctx); + } + + public interface PostInit { + TestProvider initsToReady(); + + TestProvider initsToError(boolean isError); + + default TestProvider initsToError() { + return initsToError(true); + } + + TestProvider initsToFatal(boolean isFatal); + + default TestProvider initsToFatal() { + return initsToFatal(true); + } + + TestProvider withExceptionOnFlagEvaluation(RuntimeException runtimeException); + + default TestProvider withExceptionOnFlagEvaluation() { + return withExceptionOnFlagEvaluation(new FlagNotFoundError(TestConstants.BROKEN_MESSAGE)); + } + } + + public interface InitConfig { + PostInit initWaitsFor(int millis); + + PostInit initWaitsFor(Awaitable awaitable); + } + + public interface WithFlags extends InitConfig, PostInit {} + + public interface FlagConfig { + FlagConfig withFlag(Flag flag); + + WithFlags withFlags(Flag... flags); + + WithFlags withFlags(Iterable flags); + + default WithFlags errorsOnFlagEvaluation() { + return errorsOnFlagEvaluation(true); + } + + WithFlags errorsOnFlagEvaluation(boolean error); + + WithFlags errorsOnFlagEvaluation(ErrorCode errorCode); + + WithFlags errorsOnFlagEvaluation(ErrorCode errorCode, String errorMessage); + + default WithFlags allowUnknownFlags() { + return allowUnknownFlags(true); + } + + WithFlags allowUnknownFlags(boolean allowEveryRequestedFlag); + } + + public interface WithHooks extends FlagConfig, WithFlags {} + + public interface HookConfig { + Builder withHook(Hook hook); + + WithHooks withHooks(Hook... hooks); + + WithHooks withHooks(Iterable hooks); + } + + public interface WithName extends WithHooks, HookConfig {} + + public interface NameConfig { + WithName withName(String name); + } + + public interface Builder extends WithName, NameConfig {} + + public static Builder builder() { + return new TestProviderBuilder(); + } + + public static class TestProviderBuilder implements Builder { + private final List hooks = new ArrayList<>(); + private final Map flags = new HashMap<>(); + private Awaitable initWaitsFor; + private int initDelay = 0; + private boolean errorsOnFlagEvaluation; + private ErrorCode errorCode; + private String errorMessage; + private RuntimeException runtimeException; + private boolean errorOnInit = false; + private boolean fatalOnInit = false; + private boolean allowAnyFlag = false; + private String name; + + @Override + public WithName withName(String name) { + this.name = name; + return this; + } + + @Override + public Builder withHook(Hook hook) { + this.hooks.add(hook); + return this; + } + + @Override + public WithHooks withHooks(Hook... hooks) { + for (int i = 0; i < hooks.length; i++) { + this.hooks.add(hooks[i]); + } + return this; + } + + @Override + public WithHooks withHooks(Iterable hooks) { + for (Hook hook : hooks) { + this.hooks.add(hook); + } + return this; + } + + @Override + public WithFlags errorsOnFlagEvaluation(boolean error) { + this.errorsOnFlagEvaluation = error; + return this; + } + + @Override + public WithFlags errorsOnFlagEvaluation(ErrorCode errorCode) { + this.errorsOnFlagEvaluation = true; + this.errorCode = errorCode; + return this; + } + + @Override + public WithFlags errorsOnFlagEvaluation(ErrorCode errorCode, String errorMessage) { + this.errorsOnFlagEvaluation = true; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + return this; + } + + @Override + public WithFlags allowUnknownFlags(boolean allowEveryRequestedFlag) { + this.allowAnyFlag = allowEveryRequestedFlag; + return this; + } + + @Override + public FlagConfig withFlag(Flag flag) { + flags.put(flag.name, flag); + return this; + } + + @Override + public WithFlags withFlags(Flag... flags) { + for (Flag flag : flags) { + this.flags.put(flag.name, flag); + } + return this; + } + + @Override + public WithFlags withFlags(Iterable flags) { + for (Flag flag : flags) { + this.flags.put(flag.name, flag); + } + return this; + } + + @Override + public PostInit initWaitsFor(int millis) { + initDelay = millis; + initWaitsFor = null; + return this; + } + + @Override + public PostInit initWaitsFor(Awaitable awaitable) { + initDelay = 0; + initWaitsFor = awaitable; + return this; + } + + @Override + public TestProvider initsToReady() { + errorOnInit = false; + return build(); + } + + @Override + public TestProvider initsToError(boolean isError) { + this.errorOnInit = isError; + return build(); + } + + @Override + public TestProvider initsToError() { + errorOnInit = true; + return build(); + } + + @Override + public TestProvider initsToFatal(boolean isFatal) { + this.fatalOnInit = isFatal; + return build(); + } + + @Override + public TestProvider withExceptionOnFlagEvaluation(RuntimeException runtimeException) { + this.runtimeException = runtimeException; + return build(); + } + + private TestProvider build() { + return new TestProvider( + name, + hooks, + initDelay, + initWaitsFor, + allowAnyFlag, + flags, + errorsOnFlagEvaluation, + errorOnInit, + errorCode, + errorMessage, + runtimeException, + fatalOnInit); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/vmlens/VmLensCT.java b/src/test/java/dev/openfeature/sdk/vmlens/VmLensCT.java new file mode 100644 index 000000000..c09e254e6 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/vmlens/VmLensCT.java @@ -0,0 +1,77 @@ +package dev.openfeature.sdk.vmlens; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.OpenFeatureAPITestUtil; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VmLensCT { + final OpenFeatureAPI api = OpenFeatureAPITestUtil.createAPI(); + + @BeforeEach + void setUp() { + var flags = new HashMap>(); + flags.put("a", Flag.builder().variant("a", "def").defaultVariant("a").build()); + flags.put("b", Flag.builder().variant("a", "as").defaultVariant("a").build()); + api.setProviderAndWait(new InMemoryProvider(flags)); + } + + @AfterEach + void tearDown() { + api.clearHooks(); + api.shutdown(); + } + + @Test + void concurrentClientCreations() { + try (AllInterleavings allInterleavings = new AllInterleavings("Concurrent creations of the Client")) { + while (allInterleavings.hasNext()) { + Runner.runParallel(api::getClient, api::getClient); + } + } + // keep the linter happy + assertTrue(true); + } + + @Test + void concurrentFlagEvaluations() { + var client = api.getClient(); + try (AllInterleavings allInterleavings = new AllInterleavings("Concurrent evaluations")) { + while (allInterleavings.hasNext()) { + Runner.runParallel( + () -> assertEquals("def", client.getStringValue("a", "a")), + () -> assertEquals("as", client.getStringValue("b", "b"))); + } + } + } + + @Test + void concurrentContextSetting() { + var client = api.getClient(); + var contextA = new ImmutableContext(Map.of("a", new Value("b"))); + var contextB = new ImmutableContext(Map.of("c", new Value("d"))); + try (AllInterleavings allInterleavings = + new AllInterleavings("Concurrently setting the context and evaluating a flag")) { + while (allInterleavings.hasNext()) { + Runner.runParallel( + () -> assertEquals("def", client.getStringValue("a", "a")), + () -> client.setEvaluationContext(contextA), + () -> client.setEvaluationContext(contextB)); + assertThat(client.getEvaluationContext()).isIn(contextA, contextB); + } + } + } +} diff --git a/version.txt b/version.txt index 850e74240..b57fc7228 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.14.0 +1.18.2