diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 88b02783e..bce134066 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@80c0371c57c5142ed6c844270bba1864bac8a4c6 + - uses: amannn/action-semantic-pull-request@40166f00814508ec3201fc8595b393d451c8cd80 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 9fbdbe4d7..92993ff5e 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@6d193bf28034eafb982f37bd894289fe649468fc + - uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b with: java-version: '8' distribution: 'temurin' @@ -32,7 +32,7 @@ jobs: server-password: ${{ secrets.OSSRH_PASSWORD }} - name: Cache local Maven repository - uses: actions/cache@81382a721fc89d96eca335d0c3ba33144b2baa9d + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -49,7 +49,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 7b46e54fb..4fa803aa3 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -10,22 +10,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the code - uses: actions/checkout@6d193bf28034eafb982f37bd894289fe649468fc + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b with: java-version: '8' distribution: 'temurin' cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@d8b1697e9a833a1f8cd88c642a6bd8685d3ee856 + uses: github/codeql-action/init@5b6e617dc0241b2d60c2bccea90c56b67eceb797 with: languages: java - name: Cache local Maven repository - uses: actions/cache@81382a721fc89d96eca335d0c3ba33144b2baa9d + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -36,7 +36,7 @@ jobs: run: mvn --batch-mode --update-snapshots --activate-profiles e2e verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -45,4 +45,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d8b1697e9a833a1f8cd88c642a6bd8685d3ee856 + uses: github/codeql-action/analyze@5b6e617dc0241b2d60c2bccea90c56b67eceb797 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0df4b8ae..2b1eb9e7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,10 @@ jobs: # These steps are only run if this was a merged release-please PR - name: checkout if: ${{ steps.release.outputs.release_created }} - uses: actions/checkout@6d193bf28034eafb982f37bd894289fe649468fc + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 if: ${{ steps.release.outputs.release_created }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b with: java-version: '8' distribution: 'temurin' diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 521045063..f03229c2d 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@6d193bf28034eafb982f37bd894289fe649468fc + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@d8b1697e9a833a1f8cd88c642a6bd8685d3ee856 + uses: github/codeql-action/init@5b6e617dc0241b2d60c2bccea90c56b67eceb797 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@d8b1697e9a833a1f8cd88c642a6bd8685d3ee856 + uses: github/codeql-action/autobuild@5b6e617dc0241b2d60c2bccea90c56b67eceb797 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d8b1697e9a833a1f8cd88c642a6bd8685d3ee856 + uses: github/codeql-action/analyze@5b6e617dc0241b2d60c2bccea90c56b67eceb797 diff --git a/.gitmodules b/.gitmodules index 5893173a6..476d155da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test-harness"] - path = test-harness - url = https://github.com/open-feature/test-harness +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..d58dfb70b --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9053c2177..12295c5d8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.10.0"} \ No newline at end of file +{".":"1.13.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5090ec3e2..d91800bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,216 @@ # Changelog +## [1.13.0](https://github.com/open-feature/java-sdk/compare/v1.12.2...v1.13.0) (2024-12-07) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency org.projectlombok:lombok to v1.18.36 ([#1219](https://github.com/open-feature/java-sdk/issues/1219)) ([9cadc71](https://github.com/open-feature/java-sdk/commit/9cadc71d9d8a2a88f9c716c27eb939f423b95fa0)) + + +### โœจ New Features + +* add tracking as per spec ([#1228](https://github.com/open-feature/java-sdk/issues/1228)) ([64ad644](https://github.com/open-feature/java-sdk/commit/64ad644bdbb6a4535da8ec7628e74d5f41f7ebec)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 1bd1e32 ([#1237](https://github.com/open-feature/java-sdk/issues/1237)) ([da725d8](https://github.com/open-feature/java-sdk/commit/da725d89e03d499a37307cca47b2c51af5ac8782)) +* **deps:** update actions/checkout digest to 3b9b8c8 ([#1206](https://github.com/open-feature/java-sdk/issues/1206)) ([446e298](https://github.com/open-feature/java-sdk/commit/446e2987e9b80175dff0ea72de9f58ba8e0dd323)) +* **deps:** update actions/checkout digest to cbb7224 ([#1216](https://github.com/open-feature/java-sdk/issues/1216)) ([273efc6](https://github.com/open-feature/java-sdk/commit/273efc62a7bb2e3fe962036d82818eb1da43b197)) +* **deps:** update amannn/action-semantic-pull-request digest to 40166f0 ([#1212](https://github.com/open-feature/java-sdk/issues/1212)) ([d5228f5](https://github.com/open-feature/java-sdk/commit/d5228f5ccfa55753178425c55a02af1833168513)) +* **deps:** update codecov/codecov-action action to v5 ([#1217](https://github.com/open-feature/java-sdk/issues/1217)) ([7aa77b8](https://github.com/open-feature/java-sdk/commit/7aa77b8614401c56e8387d55382e4be115a7d1ef)) +* **deps:** update codecov/codecov-action action to v5.0.2 ([#1218](https://github.com/open-feature/java-sdk/issues/1218)) ([1b4947f](https://github.com/open-feature/java-sdk/commit/1b4947f108c15a4777bb35bafb631a40c7e20e77)) +* **deps:** update codecov/codecov-action action to v5.0.3 ([#1223](https://github.com/open-feature/java-sdk/issues/1223)) ([e91194a](https://github.com/open-feature/java-sdk/commit/e91194ae16c1d68033a750050af18de68a618f61)) +* **deps:** update codecov/codecov-action action to v5.0.4 ([#1224](https://github.com/open-feature/java-sdk/issues/1224)) ([19ed5c7](https://github.com/open-feature/java-sdk/commit/19ed5c7c97dc286a85faae1c4906508f97191497)) +* **deps:** update codecov/codecov-action action to v5.0.6 ([#1226](https://github.com/open-feature/java-sdk/issues/1226)) ([13811dc](https://github.com/open-feature/java-sdk/commit/13811dcf254b604ec73b4df184d432f1dc404398)) +* **deps:** update codecov/codecov-action action to v5.0.7 ([#1227](https://github.com/open-feature/java-sdk/issues/1227)) ([234062c](https://github.com/open-feature/java-sdk/commit/234062cf338036b3b942b83c00b31191fb626432)) +* **deps:** update codecov/codecov-action action to v5.1.1 ([#1238](https://github.com/open-feature/java-sdk/issues/1238)) ([c5ad1b4](https://github.com/open-feature/java-sdk/commit/c5ad1b4d4f805a6ae070eabc6de38b37dd759c05)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.8.6.6 ([#1213](https://github.com/open-feature/java-sdk/issues/1213)) ([92c8791](https://github.com/open-feature/java-sdk/commit/92c87913ac417b8b3651290a4df828bdf5d501b9)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.10 ([#1202](https://github.com/open-feature/java-sdk/issues/1202)) ([d959059](https://github.com/open-feature/java-sdk/commit/d95905917730dcb8724fe166682ca773a536eb9b)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.8 ([#1195](https://github.com/open-feature/java-sdk/issues/1195)) ([309f28b](https://github.com/open-feature/java-sdk/commit/309f28b520a8f629a500c359b1f522ba687bcc6b)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.9 ([#1197](https://github.com/open-feature/java-sdk/issues/1197)) ([54a2345](https://github.com/open-feature/java-sdk/commit/54a234519f36ea803ec8574f27c94a9f754bf822)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.10 ([#1203](https://github.com/open-feature/java-sdk/issues/1203)) ([2bb2ed3](https://github.com/open-feature/java-sdk/commit/2bb2ed39928e0e15d369741df8b877c751e8d122)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.8 ([#1196](https://github.com/open-feature/java-sdk/issues/1196)) ([30eb2ce](https://github.com/open-feature/java-sdk/commit/30eb2ce082ae2854025be084da98fb856dbcd17c)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.9 ([#1198](https://github.com/open-feature/java-sdk/issues/1198)) ([e32a712](https://github.com/open-feature/java-sdk/commit/e32a712615f3b1be9cff61f1337d5b00c365c8f5)) +* **deps:** update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.6.0 ([#1188](https://github.com/open-feature/java-sdk/issues/1188)) ([89c7f85](https://github.com/open-feature/java-sdk/commit/89c7f85da436b9f16193948183a1ca54eea6ceef)) +* **deps:** update dependency org.apache.maven.plugins:maven-dependency-plugin to v3.8.1 ([#1187](https://github.com/open-feature/java-sdk/issues/1187)) ([5c7c287](https://github.com/open-feature/java-sdk/commit/5c7c28706e4614061b042080820b9efd04afc342)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.2 ([#1199](https://github.com/open-feature/java-sdk/issues/1199)) ([08da9a3](https://github.com/open-feature/java-sdk/commit/08da9a34395a3e96dc2172f0f0533a4905cff204)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.1 ([#1201](https://github.com/open-feature/java-sdk/issues/1201)) ([a2a57ab](https://github.com/open-feature/java-sdk/commit/a2a57ab8f1161b5de3a112bbbdc421985baf304b)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.2 ([#1240](https://github.com/open-feature/java-sdk/issues/1240)) ([c87c6e7](https://github.com/open-feature/java-sdk/commit/c87c6e7a760e84a5e8d9a6d935ef35611d1de8ab)) +* **deps:** update dependency org.apache.maven.plugins:maven-pmd-plugin to v3.26.0 ([#1189](https://github.com/open-feature/java-sdk/issues/1189)) ([d5082cd](https://github.com/open-feature/java-sdk/commit/d5082cd5f6907b6e7649813dbbea99cdeab20728)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.2 ([#1200](https://github.com/open-feature/java-sdk/issues/1200)) ([d2cb092](https://github.com/open-feature/java-sdk/commit/d2cb092b09966bc2d5a7548e35b71ab2e56e0dee)) +* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.9.1 ([#1230](https://github.com/open-feature/java-sdk/issues/1230)) ([764d665](https://github.com/open-feature/java-sdk/commit/764d6650e659aa93c1da66db348a2eb3641ae92f)) +* **deps:** update dependency org.simplify4u:slf4j2-mock to v2.4.0 ([#1208](https://github.com/open-feature/java-sdk/issues/1208)) ([a3ced47](https://github.com/open-feature/java-sdk/commit/a3ced47e5dc23badae4f008e5cf4e97c588fdfd4)) +* **deps:** update github/codeql-action digest to 024283f ([#1211](https://github.com/open-feature/java-sdk/issues/1211)) ([1df5441](https://github.com/open-feature/java-sdk/commit/1df54411b758c67afaf47f103e357cb551e0efca)) +* **deps:** update github/codeql-action digest to 3096afe ([#1235](https://github.com/open-feature/java-sdk/issues/1235)) ([409fd04](https://github.com/open-feature/java-sdk/commit/409fd042f3921948ef0dabd58d0ef7a4c380b5fb)) +* **deps:** update github/codeql-action digest to 3aa7135 ([#1186](https://github.com/open-feature/java-sdk/issues/1186)) ([4e3a329](https://github.com/open-feature/java-sdk/commit/4e3a329c406cc72a268f05766290633c67a9aae0)) +* **deps:** update github/codeql-action digest to 3d3d628 ([#1229](https://github.com/open-feature/java-sdk/issues/1229)) ([a0723ec](https://github.com/open-feature/java-sdk/commit/a0723ec2f886aa834662f2e54bcce5f052262dac)) +* **deps:** update github/codeql-action digest to 3ef4c08 ([#1205](https://github.com/open-feature/java-sdk/issues/1205)) ([eb4f625](https://github.com/open-feature/java-sdk/commit/eb4f6255615a77c65a79002f1233d1efe5eccd37)) +* **deps:** update github/codeql-action digest to 48c3e26 ([#1193](https://github.com/open-feature/java-sdk/issues/1193)) ([8621944](https://github.com/open-feature/java-sdk/commit/86219446337e9c73a41b8517b1e26fa044d3bbaa)) +* **deps:** update github/codeql-action digest to 4dc1519 ([#1209](https://github.com/open-feature/java-sdk/issues/1209)) ([1c21d24](https://github.com/open-feature/java-sdk/commit/1c21d2444b31f61d6d83dfd8f6982f7ad71f708b)) +* **deps:** update github/codeql-action digest to 5ac2ddd ([#1204](https://github.com/open-feature/java-sdk/issues/1204)) ([3a9fd60](https://github.com/open-feature/java-sdk/commit/3a9fd60fd4a9595a729995a59a0c4ef9625444bc)) +* **deps:** update github/codeql-action digest to 5cb4249 ([#1210](https://github.com/open-feature/java-sdk/issues/1210)) ([a94bd37](https://github.com/open-feature/java-sdk/commit/a94bd37cff0c6d7b9f535335709d69b79db2c91e)) +* **deps:** update github/codeql-action digest to 6a38de6 ([#1190](https://github.com/open-feature/java-sdk/issues/1190)) ([f3163df](https://github.com/open-feature/java-sdk/commit/f3163dfbd4b3997a0335699a2472373a846cf710)) +* **deps:** update github/codeql-action digest to 6e3a010 ([#1214](https://github.com/open-feature/java-sdk/issues/1214)) ([9f37927](https://github.com/open-feature/java-sdk/commit/9f37927eaa60e53d1c7db192ca8e6e117f7f0017)) +* **deps:** update github/codeql-action digest to 6f9e628 ([#1239](https://github.com/open-feature/java-sdk/issues/1239)) ([baaa78b](https://github.com/open-feature/java-sdk/commit/baaa78b7ec34a3e508fda3ed8c3ea5382f1e18ea)) +* **deps:** update github/codeql-action digest to 978ed82 ([#1234](https://github.com/open-feature/java-sdk/issues/1234)) ([bb3272d](https://github.com/open-feature/java-sdk/commit/bb3272d36479bde2594fe0bb64cea21d30299931)) +* **deps:** update github/codeql-action digest to 9f93f47 ([#1191](https://github.com/open-feature/java-sdk/issues/1191)) ([f99de6f](https://github.com/open-feature/java-sdk/commit/f99de6fa55bea093418ecc85ea79e9e30ce03d6b)) +* **deps:** update github/codeql-action digest to a1695c5 ([#1215](https://github.com/open-feature/java-sdk/issues/1215)) ([6d3bb69](https://github.com/open-feature/java-sdk/commit/6d3bb694204107f21552b48c5f6f056fa37e6cc0)) +* **deps:** update github/codeql-action digest to a6c8729 ([#1222](https://github.com/open-feature/java-sdk/issues/1222)) ([bbc934c](https://github.com/open-feature/java-sdk/commit/bbc934c6d91af39b9ff384ebd58756d48b00415a)) +* **deps:** update github/codeql-action digest to acb9cb1 ([#1207](https://github.com/open-feature/java-sdk/issues/1207)) ([21dbd3f](https://github.com/open-feature/java-sdk/commit/21dbd3fc4c29acbb6b74cdb6b82bc5bb4dd5523e)) +* **deps:** update github/codeql-action digest to af49565 ([#1231](https://github.com/open-feature/java-sdk/issues/1231)) ([4bbaf51](https://github.com/open-feature/java-sdk/commit/4bbaf517536386f53bd92ceaf62eb08fe4859e80)) +* **deps:** update github/codeql-action digest to b91f43b ([#1184](https://github.com/open-feature/java-sdk/issues/1184)) ([d0309ea](https://github.com/open-feature/java-sdk/commit/d0309eaa6616ef9e9caf8e605895ac82c8f4d780)) +* **deps:** update github/codeql-action digest to cba5fb5 ([#1221](https://github.com/open-feature/java-sdk/issues/1221)) ([37f0f06](https://github.com/open-feature/java-sdk/commit/37f0f06467b10541755e723ff26144b716a26464)) +* **deps:** update github/codeql-action digest to cbe1897 ([#1194](https://github.com/open-feature/java-sdk/issues/1194)) ([2dba3a7](https://github.com/open-feature/java-sdk/commit/2dba3a737dac6fefcbb1f56b292cacdca62735b5)) +* **deps:** update github/codeql-action digest to e782c3a ([#1220](https://github.com/open-feature/java-sdk/issues/1220)) ([45d0656](https://github.com/open-feature/java-sdk/commit/45d065652004ecc0703af3b9c6fbfd2b45e69056)) +* **deps:** update github/codeql-action digest to ef2fd42 ([#1232](https://github.com/open-feature/java-sdk/issues/1232)) ([b3549a1](https://github.com/open-feature/java-sdk/commit/b3549a1b4aa2bc27c38f66e3a0657b62d8ffc1b4)) +* **deps:** update github/codeql-action digest to f1c289a ([#1233](https://github.com/open-feature/java-sdk/issues/1233)) ([5b460ea](https://github.com/open-feature/java-sdk/commit/5b460ead7e5f21eb7c86e9ae78740a2e26957420)) +* **deps:** update github/codeql-action digest to f8e782a ([#1225](https://github.com/open-feature/java-sdk/issues/1225)) ([3227623](https://github.com/open-feature/java-sdk/commit/32276234257f82de98bcb01094c7219611e2c707)) + +## [1.12.2](https://github.com/open-feature/java-sdk/compare/v1.12.1...v1.12.2) (2024-10-24) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update junit5 monorepo ([#1171](https://github.com/open-feature/java-sdk/issues/1171)) ([02eed7a](https://github.com/open-feature/java-sdk/commit/02eed7a32c250483348d04925fe6840420b968cb)) + + +### ๐Ÿงน Chore + +* blocking set-provider in test ([d6d284b](https://github.com/open-feature/java-sdk/commit/d6d284b6a3e615ad90505bd183b098b084037616)) +* **deps:** update actions/cache digest to 6849a64 ([#1174](https://github.com/open-feature/java-sdk/issues/1174)) ([cedad9c](https://github.com/open-feature/java-sdk/commit/cedad9c2c6b6fd5c4b56b30ee5cd471fe4410a40)) +* **deps:** update actions/checkout digest to 11bd719 ([#1183](https://github.com/open-feature/java-sdk/issues/1183)) ([74958fd](https://github.com/open-feature/java-sdk/commit/74958fd261b0154d156d684e0e01360b8f3ba589)) +* **deps:** update actions/checkout digest to 163217d ([#1168](https://github.com/open-feature/java-sdk/issues/1168)) ([3f1cfed](https://github.com/open-feature/java-sdk/commit/3f1cfed913537c245284ff59d058982d1ebc8ce3)) +* **deps:** update actions/setup-java digest to 8df1039 ([#1172](https://github.com/open-feature/java-sdk/issues/1172)) ([a432760](https://github.com/open-feature/java-sdk/commit/a432760fc936b6a1c4ab2ed779c8ab49e6fe1eff)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.8.6.5 ([#1173](https://github.com/open-feature/java-sdk/issues/1173)) ([b08e8d5](https://github.com/open-feature/java-sdk/commit/b08e8d5537942e8ae8c822f0466f6e18459d2d8d)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.5 ([#1165](https://github.com/open-feature/java-sdk/issues/1165)) ([2d3be26](https://github.com/open-feature/java-sdk/commit/2d3be2617b78d200162ce816e829abda80e130a2)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.7 ([#1179](https://github.com/open-feature/java-sdk/issues/1179)) ([0db0a50](https://github.com/open-feature/java-sdk/commit/0db0a50cf40d62e9880ca68f577c43fe86497532)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.5 ([#1166](https://github.com/open-feature/java-sdk/issues/1166)) ([51a3410](https://github.com/open-feature/java-sdk/commit/51a3410d8e8c85bb0b142e6a64b889795742de86)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.7 ([#1180](https://github.com/open-feature/java-sdk/issues/1180)) ([36620f8](https://github.com/open-feature/java-sdk/commit/36620f84081bb38cc542330ea44e84f6f5754093)) +* **deps:** update dependency org.codehaus.mojo:exec-maven-plugin to v3.5.0 ([#1175](https://github.com/open-feature/java-sdk/issues/1175)) ([c8c70e2](https://github.com/open-feature/java-sdk/commit/c8c70e23e807681d271ddcb3dc6879dd80cb1c02)) +* **deps:** update github/codeql-action digest to 0a30541 ([#1170](https://github.com/open-feature/java-sdk/issues/1170)) ([59139a2](https://github.com/open-feature/java-sdk/commit/59139a21867e99e65c9460fba35403efe0aa6f50)) +* **deps:** update github/codeql-action digest to 467d7e6 ([#1181](https://github.com/open-feature/java-sdk/issues/1181)) ([7a1eb9b](https://github.com/open-feature/java-sdk/commit/7a1eb9b9e94db003e2ada37ecb32cf912eef8766)) +* **deps:** update github/codeql-action digest to af56b04 ([#1167](https://github.com/open-feature/java-sdk/issues/1167)) ([432ec43](https://github.com/open-feature/java-sdk/commit/432ec438efdbe54e2300dd78db9fff1ce73fd725)) +* **deps:** update github/codeql-action digest to b35b023 ([#1176](https://github.com/open-feature/java-sdk/issues/1176)) ([9fb469f](https://github.com/open-feature/java-sdk/commit/9fb469f8e8f45afcf55edadcef4d73753d80e0e0)) +* **deps:** update github/codeql-action digest to b7cdb7f ([#1177](https://github.com/open-feature/java-sdk/issues/1177)) ([a085896](https://github.com/open-feature/java-sdk/commit/a08589664c6464df5443eccdb1b2e9eba84313eb)) +* **deps:** update github/codeql-action digest to c470063 ([#1163](https://github.com/open-feature/java-sdk/issues/1163)) ([4e39b55](https://github.com/open-feature/java-sdk/commit/4e39b55bda516bb07ffd7452169dc77b1c0e340f)) +* fix another flaky test ([473a057](https://github.com/open-feature/java-sdk/commit/473a05784cd25dfafdd8f55894b06c8503fb19af)) +* fix flaky test ([457da96](https://github.com/open-feature/java-sdk/commit/457da96e7ba328f572e086c614b6700e9fd1c8c8)) +* flaky test ([#1169](https://github.com/open-feature/java-sdk/issues/1169)) ([d6d284b](https://github.com/open-feature/java-sdk/commit/d6d284b6a3e615ad90505bd183b098b084037616)) +* improve benchmark realism; add more context ([#1182](https://github.com/open-feature/java-sdk/issues/1182)) ([0009e23](https://github.com/open-feature/java-sdk/commit/0009e23c7b38dff78afc7addede41fed16937976)) + + +### ๐Ÿš€ Performance + +* reduce hashmap allocations ([#1178](https://github.com/open-feature/java-sdk/issues/1178)) ([fd7659a](https://github.com/open-feature/java-sdk/commit/fd7659a46fa7a8c4a04a09217abe7ab228779c7e)) + +## [1.12.1](https://github.com/open-feature/java-sdk/compare/v1.12.0...v1.12.1) (2024-10-15) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.20.0 ([#1142](https://github.com/open-feature/java-sdk/issues/1142)) ([e657383](https://github.com/open-feature/java-sdk/commit/e6573838a02df1e917b27a5ae2ad7cef0c4d6b3f)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.20.1 ([#1153](https://github.com/open-feature/java-sdk/issues/1153)) ([7ccc896](https://github.com/open-feature/java-sdk/commit/7ccc896665b56eddb16f2fd9dd63d489de9d3197)) +* **deps:** update junit5 monorepo ([#1121](https://github.com/open-feature/java-sdk/issues/1121)) ([91fffb3](https://github.com/open-feature/java-sdk/commit/91fffb35600162454b0600017bfc33b920922455)) +* **deps:** update junit5 monorepo ([#1141](https://github.com/open-feature/java-sdk/issues/1141)) ([20ea6bd](https://github.com/open-feature/java-sdk/commit/20ea6bd99dba350b41f730c548ce28aa6d7680c2)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 2cdf405 ([#1143](https://github.com/open-feature/java-sdk/issues/1143)) ([a0041c1](https://github.com/open-feature/java-sdk/commit/a0041c10e484aa2ea6bd63efcaac6c4570e1c1a4)) +* **deps:** update actions/cache digest to 8469c94 ([#1151](https://github.com/open-feature/java-sdk/issues/1151)) ([fdda5e9](https://github.com/open-feature/java-sdk/commit/fdda5e94b615e073dcc103442d61b33cc444f19f)) +* **deps:** update actions/cache digest to a11fb02 ([#1138](https://github.com/open-feature/java-sdk/issues/1138)) ([43f076a](https://github.com/open-feature/java-sdk/commit/43f076a1251569232b31e98120f29b62628717ac)) +* **deps:** update actions/checkout digest to 6b42224 ([#1137](https://github.com/open-feature/java-sdk/issues/1137)) ([0c8ff47](https://github.com/open-feature/java-sdk/commit/0c8ff472f2011f4a32ac7fb3252b9362e7ba98e3)) +* **deps:** update actions/checkout digest to d632683 ([#1122](https://github.com/open-feature/java-sdk/issues/1122)) ([2393924](https://github.com/open-feature/java-sdk/commit/2393924592b9a9cfd44ed4b4be6effeb5e7eca28)) +* **deps:** update actions/checkout digest to de5a000 ([#1134](https://github.com/open-feature/java-sdk/issues/1134)) ([626f5e1](https://github.com/open-feature/java-sdk/commit/626f5e17c0be58fe5c4b0a286a51fb85ac734c2d)) +* **deps:** update actions/checkout digest to eef6144 ([#1149](https://github.com/open-feature/java-sdk/issues/1149)) ([16ec4e4](https://github.com/open-feature/java-sdk/commit/16ec4e459b58710664db2d7831611695d6525ff5)) +* **deps:** update actions/setup-java digest to 292cc14 ([#1124](https://github.com/open-feature/java-sdk/issues/1124)) ([f2c37ea](https://github.com/open-feature/java-sdk/commit/f2c37eacc2982c47408b95839b68f33c6f7f31a5)) +* **deps:** update actions/setup-java digest to 83a06ff ([#1158](https://github.com/open-feature/java-sdk/issues/1158)) ([98a7ed0](https://github.com/open-feature/java-sdk/commit/98a7ed0727ba160642ad342f1d40e9c69980f4db)) +* **deps:** update actions/setup-java digest to b36c23c ([#1114](https://github.com/open-feature/java-sdk/issues/1114)) ([5d97803](https://github.com/open-feature/java-sdk/commit/5d9780333a04e507c2eb56253750725d14142b53)) +* **deps:** update codecov/codecov-action action to v4.6.0 ([#1132](https://github.com/open-feature/java-sdk/issues/1132)) ([f7d6202](https://github.com/open-feature/java-sdk/commit/f7d6202e131f7fd8370831c018787c0aa9deae39)) +* **deps:** update dependency com.google.guava:guava to v33.3.1-jre ([#1116](https://github.com/open-feature/java-sdk/issues/1116)) ([ce06eee](https://github.com/open-feature/java-sdk/commit/ce06eee9dfe7769f4084baf8a44e18063cbc10fc)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.2 ([#1119](https://github.com/open-feature/java-sdk/issues/1119)) ([b919333](https://github.com/open-feature/java-sdk/commit/b9193338237b7e25d415b8d81718208f885e0a51)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.3 ([#1125](https://github.com/open-feature/java-sdk/issues/1125)) ([5863541](https://github.com/open-feature/java-sdk/commit/58635411bd0f99a9a45d3832d2fee047202befff)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.4 ([#1154](https://github.com/open-feature/java-sdk/issues/1154)) ([4f32aba](https://github.com/open-feature/java-sdk/commit/4f32abaf84059696a63b7aa0d562a0bc6ef8c9f8)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.2 ([#1120](https://github.com/open-feature/java-sdk/issues/1120)) ([c5bace6](https://github.com/open-feature/java-sdk/commit/c5bace6ff258bbe7ed5c23b6abd22892de1cdc19)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.3 ([#1126](https://github.com/open-feature/java-sdk/issues/1126)) ([8765cf3](https://github.com/open-feature/java-sdk/commit/8765cf344087b0e2c76fe8df1d8440eeb85ae209)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.4 ([#1155](https://github.com/open-feature/java-sdk/issues/1155)) ([82a5eb5](https://github.com/open-feature/java-sdk/commit/82a5eb568781c057fcbfc7684d9d6283303d9e0a)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.1 ([#1147](https://github.com/open-feature/java-sdk/issues/1147)) ([aaab159](https://github.com/open-feature/java-sdk/commit/aaab1598a177e1596b052c00e054bc77f7f73f58)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.7 ([#1128](https://github.com/open-feature/java-sdk/issues/1128)) ([3816151](https://github.com/open-feature/java-sdk/commit/3816151b876282d5a2aec80e0addc8ee572ea679)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.10.1 ([#1131](https://github.com/open-feature/java-sdk/issues/1131)) ([d4dac27](https://github.com/open-feature/java-sdk/commit/d4dac274eecd65f00f2e79a591ca867eedf454c5)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.1 ([#1144](https://github.com/open-feature/java-sdk/issues/1144)) ([0c0c5f4](https://github.com/open-feature/java-sdk/commit/0c0c5f4ad9c86ed47b38b70c74c6c30dc4266866)) +* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.8.2 ([#1123](https://github.com/open-feature/java-sdk/issues/1123)) ([db1bc75](https://github.com/open-feature/java-sdk/commit/db1bc75cdeae1147a44e953d267583359b202ef6)) +* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.9.0 ([#1150](https://github.com/open-feature/java-sdk/issues/1150)) ([6d38b2c](https://github.com/open-feature/java-sdk/commit/6d38b2c5a9d578279a2eeff8a22d39eedb6aaf23)) +* **deps:** update github/codeql-action digest to 0c3e006 ([#1159](https://github.com/open-feature/java-sdk/issues/1159)) ([a881376](https://github.com/open-feature/java-sdk/commit/a8813760d6cce9124d9b90b5e9affeb87f29cb51)) +* **deps:** update github/codeql-action digest to 2617ff2 ([#1127](https://github.com/open-feature/java-sdk/issues/1127)) ([93f4feb](https://github.com/open-feature/java-sdk/commit/93f4feb818367fdf1f3f8dd55ac1bdffaf34d5f6)) +* **deps:** update github/codeql-action digest to 38469af ([#1157](https://github.com/open-feature/java-sdk/issues/1157)) ([20e3a5d](https://github.com/open-feature/java-sdk/commit/20e3a5d3fe6374e762e8439eb198c8968c2066b4)) +* **deps:** update github/codeql-action digest to 426821d ([#1115](https://github.com/open-feature/java-sdk/issues/1115)) ([a2a55e8](https://github.com/open-feature/java-sdk/commit/a2a55e8f3170172921a132edcb23197a0a03b8a3)) +* **deps:** update github/codeql-action digest to 46e0c78 ([#1118](https://github.com/open-feature/java-sdk/issues/1118)) ([90c6566](https://github.com/open-feature/java-sdk/commit/90c65666e2ec5e5dcd3cba991202fe867ebcc15d)) +* **deps:** update github/codeql-action digest to 5636274 ([#1161](https://github.com/open-feature/java-sdk/issues/1161)) ([b144763](https://github.com/open-feature/java-sdk/commit/b1447632992c1c474bb7edfad632f85a7e0c21a9)) +* **deps:** update github/codeql-action digest to 56d1975 ([#1145](https://github.com/open-feature/java-sdk/issues/1145)) ([2489e40](https://github.com/open-feature/java-sdk/commit/2489e40c290421040a8ae21ee1435055dbcd3e24)) +* **deps:** update github/codeql-action digest to 572cc52 ([#1148](https://github.com/open-feature/java-sdk/issues/1148)) ([03e6604](https://github.com/open-feature/java-sdk/commit/03e66049601c64fec4c8b516ec5557a3f82ab828)) +* **deps:** update github/codeql-action digest to 7cf65a5 ([#1140](https://github.com/open-feature/java-sdk/issues/1140)) ([9eb64a7](https://github.com/open-feature/java-sdk/commit/9eb64a747151f791d740f986c9dd358cbb813acc)) +* **deps:** update github/codeql-action digest to 8aba5f2 ([#1136](https://github.com/open-feature/java-sdk/issues/1136)) ([16e1dec](https://github.com/open-feature/java-sdk/commit/16e1dec928bf9252339bcff433cd3cb7435554d9)) +* **deps:** update github/codeql-action digest to 8b33300 ([#1139](https://github.com/open-feature/java-sdk/issues/1139)) ([1f2c5a1](https://github.com/open-feature/java-sdk/commit/1f2c5a1b2a669c65364e8e24e0824de791f4a4b4)) +* **deps:** update github/codeql-action digest to 9d1e406 ([#1152](https://github.com/open-feature/java-sdk/issues/1152)) ([e982216](https://github.com/open-feature/java-sdk/commit/e982216f705763bf1cb2638e078e54590fb4c949)) +* **deps:** update github/codeql-action digest to a196a71 ([#1133](https://github.com/open-feature/java-sdk/issues/1133)) ([c8722a2](https://github.com/open-feature/java-sdk/commit/c8722a2ac63c4aef7551ec591a1879bb60b9ad26)) +* **deps:** update github/codeql-action digest to c4d433c ([#1135](https://github.com/open-feature/java-sdk/issues/1135)) ([26659a3](https://github.com/open-feature/java-sdk/commit/26659a3eed251e6e38ce892ca8f11945ca5add90)) +* **deps:** update github/codeql-action digest to cf5b0a9 ([#1130](https://github.com/open-feature/java-sdk/issues/1130)) ([02f4ec1](https://github.com/open-feature/java-sdk/commit/02f4ec1061b82a51995389c5dad9c1abc0d35862)) +* **deps:** update github/codeql-action digest to ea2cd92 ([#1160](https://github.com/open-feature/java-sdk/issues/1160)) ([f28cefe](https://github.com/open-feature/java-sdk/commit/f28cefe3b1ea9daffccb87ff55772a2e8f7d0e81)) + + +### ๐Ÿš€ Performance + +* add heap benchmark and reduce allocations ([#1156](https://github.com/open-feature/java-sdk/issues/1156)) ([9008818](https://github.com/open-feature/java-sdk/commit/90088188c90da9fcca4e50328405e56f9ae17dde)) + +## [1.12.0](https://github.com/open-feature/java-sdk/compare/v1.11.0...v1.12.0) (2024-09-23) + + +### โœจ New Features + +* make provider interface "stateless"; SDK maintains provider state ([#1096](https://github.com/open-feature/java-sdk/issues/1096)) ([1b1e527](https://github.com/open-feature/java-sdk/commit/1b1e527e780128c9aa3c0686427a8fe8856800b4)) + + +### ๐Ÿงน Chore + +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.8.6.4 ([#1113](https://github.com/open-feature/java-sdk/issues/1113)) ([dd8ba81](https://github.com/open-feature/java-sdk/commit/dd8ba81f1286a622aec2611f023d03a56a155e89)) +* **deps:** update github/codeql-action digest to 323f5ef ([#1111](https://github.com/open-feature/java-sdk/issues/1111)) ([52e6d2b](https://github.com/open-feature/java-sdk/commit/52e6d2b0ee17124ef2a742fc872a939fde977a27)) + +## [1.11.0](https://github.com/open-feature/java-sdk/compare/v1.10.0...v1.11.0) (2024-09-20) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency io.cucumber:cucumber-bom to v7.19.0 ([#1110](https://github.com/open-feature/java-sdk/issues/1110)) ([2412f29](https://github.com/open-feature/java-sdk/commit/2412f296b5db43c07dc807390070379e7781dbb3)) + + +### โœจ New Features + +* error resolution flow control without exceptions ([#1095](https://github.com/open-feature/java-sdk/issues/1095)) ([6fc0b90](https://github.com/open-feature/java-sdk/commit/6fc0b9061079e50cbab52c951149cf600a922671)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/checkout digest to 6d193bf ([#1091](https://github.com/open-feature/java-sdk/issues/1091)) ([9f6a40e](https://github.com/open-feature/java-sdk/commit/9f6a40ec9ba490edf13adc5501a8cb2b93c32447)) +* **deps:** update actions/checkout digest to b684943 ([#1088](https://github.com/open-feature/java-sdk/issues/1088)) ([7e0c70f](https://github.com/open-feature/java-sdk/commit/7e0c70f7c547b6eb14f30ba4e77657a317161c1f)) +* **deps:** update actions/setup-java digest to 0a40ce6 ([#1107](https://github.com/open-feature/java-sdk/issues/1107)) ([37b56ac](https://github.com/open-feature/java-sdk/commit/37b56ac2dad3c24ac1d898a253e936aa2e3d49eb)) +* **deps:** update actions/setup-java digest to 2dfa201 ([#1094](https://github.com/open-feature/java-sdk/issues/1094)) ([082f574](https://github.com/open-feature/java-sdk/commit/082f5746c84f1bb4ff54cae4f525949f29e7b9c4)) +* **deps:** update actions/setup-java digest to 40b9536 ([#1109](https://github.com/open-feature/java-sdk/issues/1109)) ([244f216](https://github.com/open-feature/java-sdk/commit/244f216582eae071885a950712115ce8b1ad0c19)) +* **deps:** update actions/setup-java digest to 7467385 ([#1092](https://github.com/open-feature/java-sdk/issues/1092)) ([58ead7f](https://github.com/open-feature/java-sdk/commit/58ead7fa9153a53ec530e349f16f9e5e183aa0fb)) +* **deps:** update actions/setup-java digest to bcfbca5 ([#1100](https://github.com/open-feature/java-sdk/issues/1100)) ([c68f78e](https://github.com/open-feature/java-sdk/commit/c68f78e17b6125d74644a5735f9d58348edcc383)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.6 ([#1103](https://github.com/open-feature/java-sdk/issues/1103)) ([29901b8](https://github.com/open-feature/java-sdk/commit/29901b87ea93757abe5f5d7a8afc6fc4555aef5c)) +* **deps:** update github/codeql-action digest to 4a01ec7 ([#1101](https://github.com/open-feature/java-sdk/issues/1101)) ([b80fd6d](https://github.com/open-feature/java-sdk/commit/b80fd6d307dee87d8b8f148c954c33b8ff6efae2)) +* **deps:** update github/codeql-action digest to 5618c9f ([#1102](https://github.com/open-feature/java-sdk/issues/1102)) ([d1478c0](https://github.com/open-feature/java-sdk/commit/d1478c001a8e88c358d6743ac3d7c9b5523a893e)) +* **deps:** update github/codeql-action digest to 64431c6 ([#1106](https://github.com/open-feature/java-sdk/issues/1106)) ([ce19ac9](https://github.com/open-feature/java-sdk/commit/ce19ac91f62689a0e0e02e53538e1035bf95387f)) +* **deps:** update github/codeql-action digest to 782de45 ([#1104](https://github.com/open-feature/java-sdk/issues/1104)) ([7cb8908](https://github.com/open-feature/java-sdk/commit/7cb89087daa2809d3fd8447388e58e4f73c975b3)) +* **deps:** update github/codeql-action digest to 799e477 ([#1108](https://github.com/open-feature/java-sdk/issues/1108)) ([17a58ef](https://github.com/open-feature/java-sdk/commit/17a58efc7e0244c0db77e77a78f6d0daf948028f)) +* **deps:** update github/codeql-action digest to 8fd294e ([#1097](https://github.com/open-feature/java-sdk/issues/1097)) ([6408261](https://github.com/open-feature/java-sdk/commit/64082617fade3fa7d647302cc0c5b698f7038d81)) +* **deps:** update github/codeql-action digest to 9b41ced ([#1089](https://github.com/open-feature/java-sdk/issues/1089)) ([870fc27](https://github.com/open-feature/java-sdk/commit/870fc27ed7a30cb011080127f7d040c1a3bb209b)) +* **deps:** update github/codeql-action digest to cb28816 ([#1105](https://github.com/open-feature/java-sdk/issues/1105)) ([9d69ebd](https://github.com/open-feature/java-sdk/commit/9d69ebd94a161e6477f2b4f1f5edd0b79f178852)) +* **deps:** update github/codeql-action digest to d8b1697 ([#1093](https://github.com/open-feature/java-sdk/issues/1093)) ([d60593f](https://github.com/open-feature/java-sdk/commit/d60593fa11f39c6587e280633cf6e15965c0960f)) +* **deps:** update github/codeql-action digest to e817992 ([#1099](https://github.com/open-feature/java-sdk/issues/1099)) ([caec6e3](https://github.com/open-feature/java-sdk/commit/caec6e35e9da3ad88a8de105ca170137f42b907c)) + ## [1.10.0](https://github.com/open-feature/java-sdk/compare/v1.9.1...v1.10.0) (2024-09-05) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81d5d4625..84c9645b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,9 @@ Any contributions you make are expected to be tested with unit tests. You can va Further, it is recommended to verify code styling and static code analysis with `mvn verify -P !deploy`. Regardless, the automation itself will run them for you when you open a PR. +> [!TIP] +> For easier usage maven wrapper is available. Example usage: `./mvnw verify` + Your code is supposed to work with Java 8+. If you think we might be out of date with the spec, you can check that by invoking `python spec_finder.py` in the root of the repository. This will validate we have tests defined for all of the specification entries we know about. @@ -20,13 +23,26 @@ If you're adding tests to cover something in the spec, use the `@Specification` ## End-to-End Tests -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/spec/blob/main/specification/assets/gherkin/evaluation.feature) using `InMemoryProvider`. to run alone: ``` mvn test -P e2e ``` +## Benchmarking + +There is a small JMH benchmark suite for testing allocations that can be run with: + +```sh +mvn -P benchmark clean compile test-compile jmh:benchmark -Djmh.f=1 -Djmh.prof='dev.openfeature.sdk.benchmark.AllocationProfiler' +``` + +If you are concerned about the repercussions of a change on memory usage, run this an compare the results to the committed. `benchmark.txt` file. +Note that the ONLY MEANINGFUL RESULTS of this benchmark are the `totalAllocatedBytes` and the `totalAllocatedInstances`. +The `run` score, and maven task time are not relevant since this benchmark is purely memory-related and has nothing to do with speed. +You can also view the heap breakdown to see which objects are taking up the most memory. + ## Releasing See [releasing](./docs/release.md). diff --git a/README.md b/README.md index a7e984624..e58ef9d8c 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - Release + + Release @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.10.0 + 1.13.0 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.10.0' + implementation 'dev.openfeature:sdk:1.13.0' } ``` @@ -120,17 +120,18 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs. ## ๐ŸŒŸ Features -| Status | Features | Description | -| ------ |-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| โœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| โœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| โœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| โœ… | [Logging](#logging) | Integrate with popular logging packages. | -| โœ… | [Domains](#domains) | Logically bind clients with providers. | -| โœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| โœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| โœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | -| โœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ |---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| โœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| โœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| โœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| โœ… | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | +| โœ… | [Logging](#logging) | Integrate with popular logging packages. | +| โœ… | [Domains](#domains) | Logically bind clients with providers. | +| โœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| โœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| โœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | +| โœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: โœ… | In-progress: โš ๏ธ | Not implemented yet: โŒ @@ -215,6 +216,16 @@ Once you've added a hook as a dependency, it can be registered at the global, cl FlagEvaluationOptions.builder().hook(new ExampleHook()).build()); ``` +### Tracking + +The [tracking API](https://openfeature.dev/specification/sections/tracking/) allows you to use OpenFeature abstractions to associate user actions with feature flag evaluations. +This is essential for robust experimentation powered by feature flags. Note that, unlike methods that handle feature flag evaluations, calling `track(...)` may throw an `IllegalArgumentException` if an empty string is passed as the `trackingEventName`. + +```java +OpenFeatureAPI api = OpenFeatureAPI.getInstance(); +api.getClient().track("visited-promo-page", new MutableTrackingEventDetails(99.77).add("currency", "USD")); +``` + ### Logging The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation. @@ -317,11 +328,6 @@ public class MyProvider implements FeatureProvider { return () -> "My Provider"; } - @Override - public ProviderState getState() { - // optionally indicate your provider's state (assumed to be READY if not implemented) - } - @Override public void initialize(EvaluationContext evaluationContext) throws Exception { // start up your provider @@ -368,11 +374,6 @@ class MyEventProvider extends EventProvider { return () -> "My Event Provider"; } - @Override - public ProviderState getState() { - // indicate your provider's state (required for EventProviders) - } - @Override public void initialize(EvaluationContext evaluationContext) throws Exception { // emit events when flags are changed in a hypothetical REST API @@ -391,6 +392,13 @@ class MyEventProvider extends EventProvider { } ``` +Providers no longer need to manage their own state, this is done by the SDK itself. If desired, the state of a provider +can be queried through the client that uses the provider. + +```java +OpenFeatureAPI.getInstance().getClient().getProviderState(); +``` + > Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs! ### Develop a hook diff --git a/benchmark.txt b/benchmark.txt new file mode 100644 index 000000000..e43e684d0 --- /dev/null +++ b/benchmark.txt @@ -0,0 +1,294 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------< dev.openfeature:sdk >------------------------- +[INFO] Building OpenFeature Java SDK 1.12.1 +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)' +[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)' +[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)' +[INFO] +[INFO] --- clean:3.2.0:clean (default-clean) @ sdk --- +[INFO] Deleting /home/todd/git/java-sdk/target +[INFO] +[INFO] --- checkstyle:3.5.0:check (validate) @ sdk --- +[INFO] Starting audit... +Audit done. +[INFO] You have 0 Checkstyle violations. +[INFO] +[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk --- +[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ sdk --- +[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk --- +[INFO] Recompiling the module because of changed source code. +[INFO] Compiling 65 source files with javac [debug target 1.8] to target/classes +[WARNING] bootstrap class path not set in conjunction with -source 8 +[WARNING] source value 8 is obsolete and will be removed in a future release +[WARNING] target value 8 is obsolete and will be removed in a future release +[WARNING] To suppress warnings about obsolete options, use -Xlint:-options. +[INFO] Annotation processing is enabled because one or more processors were found + on the class path. A future release of javac may disable annotation processing + unless at least one processor is specified by name (-processor), or a search + path is specified (--processor-path, --processor-module-path), or annotation + processing is enabled explicitly (-proc:only, -proc:full). + Use -Xlint:-options to suppress this message. + Use -proc:none to disable annotation processing. +[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/MutableStructure.java:[19,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type. +[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/ImmutableStructure.java:[22,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type. +[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/EventDetails.java:[9,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type. +[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java:[27,26] finalize() in java.lang.Object has been deprecated and marked for removal +[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Some input files use or override a deprecated API. +[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Recompile with -Xlint:deprecation for details. +[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Some input files use unchecked or unsafe operations. +[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Recompile with -Xlint:unchecked for details. +[INFO] +[INFO] --- checkstyle:3.5.0:check (validate) @ sdk --- +[INFO] Starting audit... +Audit done. +[INFO] You have 0 Checkstyle violations. +[INFO] +[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk --- +[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ sdk --- +[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk --- +[INFO] Copying 2 resources from src/test/resources to target/test-classes +[INFO] +[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ sdk --- +[INFO] Recompiling the module because of changed dependency. +[INFO] Compiling 52 source files with javac [debug target 1.8] to target/test-classes +[WARNING] bootstrap class path not set in conjunction with -source 8 +[WARNING] source value 8 is obsolete and will be removed in a future release +[WARNING] target value 8 is obsolete and will be removed in a future release +[WARNING] To suppress warnings about obsolete options, use -Xlint:-options. +[INFO] Annotation processing is enabled because one or more processors were found + on the class path. A future release of javac may disable annotation processing + unless at least one processor is specified by name (-processor), or a search + path is specified (--processor-path, --processor-module-path), or annotation + processing is enabled explicitly (-proc:only, -proc:full). + Use -Xlint:-options to suppress this message. + Use -proc:none to disable annotation processing. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/EventsTest.java: Some input files use or override a deprecated API. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/EventsTest.java: Recompile with -Xlint:deprecation for details. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Some input files use unchecked or unsafe operations. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Recompile with -Xlint:unchecked for details. +[INFO] +[INFO] >>> jmh:0.2.2:benchmark (default-cli) > process-test-resources @ sdk >>> +[INFO] +[INFO] --- checkstyle:3.5.0:check (validate) @ sdk --- +[INFO] Starting audit... +Audit done. +[INFO] You have 0 Checkstyle violations. +[INFO] +[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk --- +[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ sdk --- +[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk --- +[INFO] Copying 2 resources from src/test/resources to target/test-classes +[INFO] +[INFO] <<< jmh:0.2.2:benchmark (default-cli) < process-test-resources @ sdk <<< +[INFO] +[INFO] +[INFO] --- jmh:0.2.2:benchmark (default-cli) @ sdk --- +[INFO] Changes detected - recompiling the module! +[INFO] Compiling 52 source files to /home/todd/git/java-sdk/target/test-classes +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Some input files use or override a deprecated API. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Recompile with -Xlint:deprecation for details. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Some input files use unchecked or unsafe operations. +[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Recompile with -Xlint:unchecked for details. +[INFO] Executing the JMH benchmarks +# JMH version: 1.37 +# VM version: JDK 21.0.4, OpenJDK 64-Bit Server VM, 21.0.4+7 +# VM invoker: /usr/lib/jvm/java-21-openjdk/bin/java +# VM options: -Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC +# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable) +# Warmup: +# Measurement: 1 iterations, single-shot each +# Timeout: 10 min per iteration +# Threads: 1 thread +# Benchmark mode: Single shot invocation time +# Benchmark: dev.openfeature.sdk.benchmark.AllocationBenchmark.run + +# Run progress: 0.00% complete, ETA 00:00:00 +# Fork: 1 of 1 +[0.001s][warning][gc,init] Consider setting -Xms equal to -Xmx to avoid resizing hiccups +[0.001s][warning][gc,init] Consider enabling -XX:+AlwaysPreTouch to avoid memory commit hiccups +Iteration 1: num #instances #bytes class name (module) +------------------------------------------------------- + 1: 480234 23051232 java.util.HashMap (java.base@21.0.4) + 2: 150497 12050088 [Ljava.util.HashMap$Node; (java.base@21.0.4) + 3: 332017 10624544 java.util.HashMap$Node (java.base@21.0.4) + 4: 47815 9732480 [B (java.base@21.0.4) + 5: 305991 8105872 [Ljava.lang.Object; (java.base@21.0.4) + 6: 366682 5866912 java.util.Optional (java.base@21.0.4) + 7: 183332 5866624 java.util.HashMap$EntryIterator (java.base@21.0.4) + 8: 172970 5535040 java.util.Collections$UnmodifiableMap (java.base@21.0.4) + 9: 100000 4000000 dev.openfeature.sdk.HookContext + 10: 100000 4000000 dev.openfeature.sdk.HookContext$HookContextBuilder + 11: 230006 3680096 dev.openfeature.sdk.Value + 12: 200062 3200992 java.util.HashMap$EntrySet (java.base@21.0.4) + 13: 132870 3188880 java.util.ArrayList (java.base@21.0.4) + 14: 192292 3076672 dev.openfeature.sdk.ImmutableStructure + 15: 182292 2916672 dev.openfeature.sdk.ImmutableContext + 16: 50000 2000000 dev.openfeature.sdk.FlagEvaluationDetails + 17: 50000 2000000 dev.openfeature.sdk.ProviderEvaluation + 18: 122968 1967488 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet (java.base@21.0.4) + 19: 149 1884376 [Ljdk.internal.vm.FillerElement; (java.base@21.0.4) + 20: 56476 1807232 java.util.ArrayList$Itr (java.base@21.0.4) + 21: 37481 1799088 dev.openfeature.sdk.FlagEvaluationDetails$FlagEvaluationDetailsBuilder + 22: 100001 1600016 dev.openfeature.sdk.NoOpProvider$$Lambda/0x000076e79c02fa78 + 23: 50000 1600000 [Ldev.openfeature.sdk.EvaluationContext; + 24: 50000 1600000 [Ljava.util.List; (java.base@21.0.4) + 25: 100000 1600000 dev.openfeature.sdk.OpenFeatureClient$$Lambda/0x000076e79c082800 + 26: 36720 1468800 dev.openfeature.sdk.ProviderEvaluation$ProviderEvaluationBuilder + 27: 87481 1399696 dev.openfeature.sdk.ImmutableMetadata + 28: 50000 1200000 dev.openfeature.sdk.FlagEvaluationOptions + 29: 74201 1187216 dev.openfeature.sdk.ImmutableMetadata$ImmutableMetadataBuilder + 30: 73235 1171760 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntry (java.base@21.0.4) + 31: 45869 1100856 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$1 (java.base@21.0.4) + 32: 43776 1050624 dev.openfeature.sdk.FlagEvaluationOptions$FlagEvaluationOptionsBuilder + 33: 40016 960384 dev.openfeature.sdk.HookSupport$$Lambda/0x000076e79c081b78 + 34: 39967 959208 dev.openfeature.sdk.HookSupport$$Lambda/0x000076e79c081da8 + 35: 57783 924528 dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock$$Lambda/0x000076e79c02eae8 + 36: 4490 721440 [I (java.base@21.0.4) + 37: 26594 638256 java.lang.String (java.base@21.0.4) + 38: 1461 390008 [J (java.base@21.0.4) + 39: 2361 288784 java.lang.Class (java.base@21.0.4) + 40: 4632 259392 jdk.internal.org.objectweb.asm.SymbolTable$Entry (java.base@21.0.4) + 41: 10001 240024 java.lang.Double (java.base@21.0.4) + 42: 2502 180144 java.lang.reflect.Field (java.base@21.0.4) + 43: 6007 144168 java.lang.StringBuilder (java.base@21.0.4) + 44: 180 140968 [Ljdk.internal.org.objectweb.asm.SymbolTable$Entry; (java.base@21.0.4) + 45: 3827 122464 java.util.concurrent.ConcurrentHashMap$Node (java.base@21.0.4) + 46: 48 122168 [C (java.base@21.0.4) + 47: 1440 113512 [S (java.base@21.0.4) + 48: 1201 105688 java.lang.reflect.Method (java.base@21.0.4) + 49: 3031 79600 [Ljava.lang.Class; (java.base@21.0.4) + 50: 1351 75656 jdk.internal.org.objectweb.asm.Label (java.base@21.0.4) + 51: 1561 74928 java.lang.invoke.MemberName (java.base@21.0.4) + 52: 334 74816 jdk.internal.org.objectweb.asm.MethodWriter (java.base@21.0.4) + 53: 1799 71960 java.lang.invoke.MethodType (java.base@21.0.4) + 54: 1089 69696 java.net.URL (java.base@21.0.4) + 55: 121 50512 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@21.0.4) + 56: 3147 50352 jdk.internal.util.StrongReferenceKey (java.base@21.0.4) + 57: 1057 42280 java.io.ObjectStreamField (java.base@21.0.4) + 58: 1225 39200 java.io.File (java.base@21.0.4) + 59: 779 37392 jdk.internal.org.objectweb.asm.Frame (java.base@21.0.4) + 60: 243 25272 java.util.jar.JarFile$JarFileEntry (java.base@21.0.4) + 61: 794 25248 [Ljava.lang.String; (java.base@21.0.4) + 62: 622 24880 java.lang.NoSuchFieldException (java.base@21.0.4) + 63: 571 22840 java.util.LinkedHashMap$Entry (java.base@21.0.4) + 64: 474 22752 jdk.internal.ref.CleanerImpl$PhantomCleanableRef (java.base@21.0.4) + 65: 690 22080 jdk.internal.util.WeakReferenceKey (java.base@21.0.4) + 66: 828 19872 jdk.internal.org.objectweb.asm.ByteVector (java.base@21.0.4) + 67: 248 18848 [Ljava.lang.ref.SoftReference; (java.base@21.0.4) + 68: 118 17936 jdk.internal.org.objectweb.asm.ClassWriter (java.base@21.0.4) + 69: 380 16824 [Ljava.lang.invoke.LambdaForm$Name; (java.base@21.0.4) + 70: 625 15000 java.lang.Long (java.base@21.0.4) + 71: 463 14816 java.lang.invoke.LambdaForm$Name (java.base@21.0.4) + 72: 904 14464 java.lang.Object (java.base@21.0.4) + 73: 198 14256 java.lang.reflect.Constructor (java.base@21.0.4) + 74: 249 13944 java.util.zip.ZipFile$ZipFileInputStream (java.base@21.0.4) + 75: 334 13360 jdk.internal.org.objectweb.asm.Handler (java.base@21.0.4) + 76: 202 12928 java.util.concurrent.ConcurrentHashMap (java.base@21.0.4) + 77: 201 12864 jdk.internal.org.objectweb.asm.FieldWriter (java.base@21.0.4) + 78: 316 12640 java.util.WeakHashMap$Entry (java.base@21.0.4) + 79: 102 12240 java.io.ObjectStreamClass (java.base@21.0.4) + 80: 249 11952 java.util.zip.ZipFile$ZipFileInflaterInputStream (java.base@21.0.4) + 81: 359 11488 jdk.internal.org.objectweb.asm.Type (java.base@21.0.4) + 82: 465 11160 java.lang.invoke.ResolvedMethodName (java.base@21.0.4) + 83: 464 11136 jdk.internal.org.objectweb.asm.Edge (java.base@21.0.4) + 84: 341 10912 jdk.internal.math.FDBigInteger (java.base@21.0.4) + 85: 94 10728 [Ljava.lang.reflect.Field; (java.base@21.0.4) + 86: 266 10640 java.lang.NoSuchMethodException (java.base@21.0.4) + 87: 266 10640 java.security.CodeSource (java.base@21.0.4) + 88: 221 10608 java.lang.invoke.DirectMethodHandle$Constructor (java.base@21.0.4) + 89: 264 10560 sun.security.util.KnownOIDs (java.base@21.0.4) + 90: 75 10200 sun.nio.fs.UnixFileAttributes (java.base@21.0.4) + 91: 245 9800 java.lang.ref.SoftReference (java.base@21.0.4) + 92: 118 9440 jdk.internal.event.DeserializationEvent (java.base@21.0.4) + 93: 115 9200 [Ljava.util.WeakHashMap$Entry; (java.base@21.0.4) + 94: 368 8832 java.lang.module.ModuleDescriptor$Exports (java.base@21.0.4) + 95: 63 8384 [Ljava.lang.invoke.MethodHandle; (java.base@21.0.4) + 96: 146 8176 java.io.FileCleanable (java.base@21.0.4) + 97: 125 8000 java.lang.Class$ReflectionData (java.base@21.0.4) + 98: 323 7752 java.util.ImmutableCollections$Set12 (java.base@21.0.4) + 99: 121 7744 jdk.internal.org.objectweb.asm.SymbolTable (java.base@21.0.4) + 100: 70 7280 java.lang.invoke.InnerClassLambdaMetafactory (java.base@21.0.4) + 101: 144 6912 jdk.internal.org.objectweb.asm.AnnotationWriter (java.base@21.0.4) + 102: 167 6680 jdk.internal.loader.URLClassPath$JarLoader$2 (java.base@21.0.4) + 103: 199 6368 java.lang.invoke.MethodHandles$Lookup (java.base@21.0.4) + 104: 156 6240 java.util.StringJoiner (java.base@21.0.4) + 105: 153 6120 java.io.FileDescriptor (java.base@21.0.4) + 106: 126 6048 java.lang.invoke.LambdaForm (java.base@21.0.4) + 107: 77 6016 [Ljava.lang.reflect.Method; (java.base@21.0.4) + 108: 249 5976 java.util.zip.ZipFile$InflaterCleanupAction (java.base@21.0.4) + 109: 373 5968 java.lang.Byte (java.base@21.0.4) + 110: 74 5920 java.util.zip.ZipFile$Source (java.base@21.0.4) + 111: 82 5720 [Ljava.io.ObjectStreamField; (java.base@21.0.4) + 112: 40 5640 [Ljava.lang.ClassValue$Entry; (java.base@21.0.4) + 113: 234 5616 java.util.jar.Attributes$Name (java.base@21.0.4) + 114: 174 5568 java.util.concurrent.locks.ReentrantLock$NonfairSync (java.base@21.0.4) + 115: 98 5488 java.lang.Module (java.base@21.0.4) + 116: 219 5256 java.lang.PublicMethods$MethodList (java.base@21.0.4) + 117: 65 5200 java.net.URI (java.base@21.0.4) + 118: 215 5104 [Ljdk.internal.org.objectweb.asm.Type; (java.base@21.0.4) +truncated... +Total 4452140 139359040 + +0.186 s/op + +totalAllocatedBytes: 139359040.000 bytes + +totalAllocatedInstances: 4452140.000 instances + +totalHeap: 521412608.000 bytes + + + +Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedBytes": + 139359040.000 bytes + +Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedInstances": + 4452140.000 instances + +Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalHeap": + 521412608.000 bytes + + +# Run complete. Total time: 00:00:00 + +REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on +why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial +experiments, perform baseline and negative tests that provide experimental control, make sure +the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts. +Do not assume the numbers tell you what you want them to tell. + +NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise +extra caution when trusting the results, look into the generated code to check the benchmark still +works, and factor in a small probability of new VM bugs. Additionally, while comparisons between +different JVMs are already problematic, the performance difference caused by different Blackhole +modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons. + +Benchmark Mode Cnt Score Error Units +AllocationBenchmark.run ss 0.186 s/op +AllocationBenchmark.run:+totalAllocatedBytes ss 139359040.000 bytes +AllocationBenchmark.run:+totalAllocatedInstances ss 4452140.000 instances +AllocationBenchmark.run:+totalHeap ss 521412608.000 bytes +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 8.280 s +[INFO] Finished at: 2024-10-23T12:37:24-04:00 +[INFO] ------------------------------------------------------------------------ diff --git a/mvnw b/mvnw new file mode 100644 index 000000000..19529ddf8 --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# 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" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + 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/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + 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 + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +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" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 000000000..249bdf382 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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" diff --git a/pom.xml b/pom.xml index 4830a7ad2..e499052f2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,17 @@ - + 4.0.0 dev.openfeature sdk - 1.10.0 + 1.13.0 UTF-8 1.8 ${maven.compiler.source} - 5.11.0 - + 5.11.4 + **/e2e/*.java ${project.groupId}.${project.artifactId} @@ -45,7 +45,7 @@ org.projectlombok lombok - 1.18.34 + 1.18.36 provided @@ -74,7 +74,7 @@ org.assertj assertj-core - 3.26.3 + 3.27.0 test @@ -109,7 +109,7 @@ org.junit.platform junit-platform-suite - 1.11.0 + 1.11.4 test @@ -128,14 +128,14 @@ org.simplify4u slf4j2-mock - 2.3.0 + 2.4.0 test com.google.guava guava - 33.3.0-jre + 33.4.0-jre test @@ -146,6 +146,13 @@ test + + org.openjdk.jmh + jmh-core + 1.37 + test + + @@ -157,14 +164,14 @@ net.bytebuddy byte-buddy - 1.15.1 + 1.15.11 test net.bytebuddy byte-buddy-agent - 1.15.1 + 1.15.11 test @@ -172,7 +179,7 @@ io.cucumber cucumber-bom - 7.18.1 + 7.20.1 pom import @@ -180,7 +187,7 @@ org.junit junit-bom - 5.11.0 + 5.11.4 pom import @@ -193,7 +200,7 @@ org.cyclonedx cyclonedx-maven-plugin - 2.8.1 + 2.9.1 library 1.3 @@ -218,7 +225,7 @@ maven-dependency-plugin - 3.8.0 + 3.8.1 verify @@ -253,7 +260,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.0 + 3.5.2 ${surefireArgLine} @@ -268,7 +275,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.5.0 + 3.5.2 ${surefireArgLine} @@ -352,7 +359,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.25.0 + 3.26.0 run-pmd @@ -367,7 +374,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.6.3 + 4.8.6.6 spotbugs-exclusions.xml @@ -400,7 +407,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.5.0 + 3.6.0 checkstyle.xml UTF-8 @@ -470,10 +477,10 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.0 + 3.11.2 true - all,-missing + all,-missing @@ -490,7 +497,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.5 + 3.2.7 sign-artifacts @@ -546,6 +553,19 @@ + + benchmark + + + + pw.krejci + jmh-maven-plugin + 0.2.2 + + + + + e2e @@ -558,7 +578,7 @@ org.codehaus.mojo exec-maven-plugin - 3.4.1 + 3.5.0 update-test-harness-submodule @@ -573,22 +593,21 @@ submodule update --init - test-harness + spec - copy-gherkin-tests + copy-evaluation-gherkin-tests validate exec - cp - test-harness/features/evaluation.feature + spec/specification/assets/gherkin/evaluation.feature src/test/resources/features/ diff --git a/spec b/spec new file mode 160000 index 000000000..d4a9a9109 --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit d4a9a910946eded57cf82d6fd4921785a5e64c2b diff --git a/src/main/java/dev/openfeature/sdk/AbstractStructure.java b/src/main/java/dev/openfeature/sdk/AbstractStructure.java index e50fbe920..86fdde41a 100644 --- a/src/main/java/dev/openfeature/sdk/AbstractStructure.java +++ b/src/main/java/dev/openfeature/sdk/AbstractStructure.java @@ -2,18 +2,32 @@ import java.util.HashMap; import java.util.Map; +import java.util.Collections; @SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" }) abstract class AbstractStructure implements Structure { protected final Map attributes; + @Override + public boolean isEmpty() { + return attributes == null || attributes.size() == 0; + } + AbstractStructure() { this.attributes = new HashMap<>(); } AbstractStructure(Map attributes) { - this.attributes = new HashMap<>(attributes); + this.attributes = attributes; + } + + /** + * Returns an unmodifiable representation of the internal attribute map. + * @return immutable map + */ + public Map asUnmodifiableMap() { + return Collections.unmodifiableMap(attributes); } /** @@ -32,4 +46,5 @@ public Map asObjectMap() { (accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())), HashMap::putAll); } + } diff --git a/src/main/java/dev/openfeature/sdk/Client.java b/src/main/java/dev/openfeature/sdk/Client.java index 2184cdcbb..441d31e2b 100644 --- a/src/main/java/dev/openfeature/sdk/Client.java +++ b/src/main/java/dev/openfeature/sdk/Client.java @@ -5,17 +5,19 @@ /** * Interface used to resolve flags of varying types. */ -public interface Client extends Features, EventBus { +public interface Client extends Features, Tracking, EventBus { ClientMetadata getMetadata(); /** * Return an optional client-level evaluation context. + * * @return {@link EvaluationContext} */ EvaluationContext getEvaluationContext(); /** * Set the client-level evaluation context. + * * @param ctx Client level context. */ Client setEvaluationContext(EvaluationContext ctx); @@ -30,7 +32,15 @@ public interface Client extends Features, EventBus { /** * Fetch the hooks associated to this client. + * * @return A list of {@link Hook}s. */ List getHooks(); + + /** + * Returns the current state of the associated provider. + * + * @return the provider state + */ + ProviderState getProviderState(); } diff --git a/src/main/java/dev/openfeature/sdk/ErrorCode.java b/src/main/java/dev/openfeature/sdk/ErrorCode.java index 6b387e27d..00451bdfb 100644 --- a/src/main/java/dev/openfeature/sdk/ErrorCode.java +++ b/src/main/java/dev/openfeature/sdk/ErrorCode.java @@ -2,5 +2,6 @@ @SuppressWarnings("checkstyle:MissingJavadocType") public enum ErrorCode { - PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL + PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL, + PROVIDER_FATAL } diff --git a/src/main/java/dev/openfeature/sdk/EvaluationContext.java b/src/main/java/dev/openfeature/sdk/EvaluationContext.java index b95ea454d..5b2a33113 100644 --- a/src/main/java/dev/openfeature/sdk/EvaluationContext.java +++ b/src/main/java/dev/openfeature/sdk/EvaluationContext.java @@ -1,5 +1,9 @@ package dev.openfeature.sdk; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; + /** * The EvaluationContext is a container for arbitrary contextual data * that can be used as a basis for dynamic evaluation. @@ -19,4 +23,39 @@ public interface EvaluationContext extends Structure { * @return resulting merged context */ EvaluationContext merge(EvaluationContext overridingContext); + + /** + * Recursively merges the overriding map into the base Value map. + * The base map is mutated, the overriding map is not. + * Null maps will cause no-op. + * + * @param newStructure function to create the right structure(s) for Values + * @param base base map to merge + * @param overriding overriding map to merge + */ + static void mergeMaps(Function, Structure> newStructure, + Map base, + Map overriding) { + + if (base == null) { + return; + } + if (overriding == null || overriding.isEmpty()) { + return; + } + + for (Entry overridingEntry : overriding.entrySet()) { + String key = overridingEntry.getKey(); + if (overridingEntry.getValue().isStructure() && base.containsKey(key) && base.get(key).isStructure()) { + Structure mergedValue = base.get(key).asStructure(); + Structure overridingValue = overridingEntry.getValue().asStructure(); + Map newMap = mergedValue.asMap(); + mergeMaps(newStructure, newMap, + overridingValue.asUnmodifiableMap()); + base.put(key, new Value(newStructure.apply(newMap))); + } else { + base.put(key, overridingEntry.getValue()); + } + } + } } diff --git a/src/main/java/dev/openfeature/sdk/EventProvider.java b/src/main/java/dev/openfeature/sdk/EventProvider.java index 4e8c022b4..84893bece 100644 --- a/src/main/java/dev/openfeature/sdk/EventProvider.java +++ b/src/main/java/dev/openfeature/sdk/EventProvider.java @@ -2,6 +2,7 @@ import dev.openfeature.sdk.internal.TriConsumer; + /** * Abstract EventProvider. Providers must extend this class to support events. * Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please @@ -15,22 +16,20 @@ * @see FeatureProvider */ public abstract class EventProvider implements FeatureProvider { + private EventProviderListener eventProviderListener; - /** - * {@inheritDoc} - */ - @Override - public abstract ProviderState getState(); + void setEventProviderListener(EventProviderListener eventProviderListener) { + this.eventProviderListener = eventProviderListener; + } private TriConsumer onEmit = null; /** * "Attach" this EventProvider to an SDK, which allows events to propagate from this provider. - * No-op if the same onEmit is already attached. + * No-op if the same onEmit is already attached. * * @param onEmit the function to run when a provider emits events. * @throws IllegalStateException if attempted to bind a new emitter for already bound provider - * */ void attach(TriConsumer onEmit) { if (this.onEmit != null && this.onEmit != onEmit) { @@ -50,11 +49,14 @@ void detach() { /** * 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); } @@ -63,7 +65,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) { /** * Emit a {@link ProviderEvent#PROVIDER_READY} event. * Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)} - * + * * @param details The details of the event */ public void emitProviderReady(ProviderEventDetails details) { @@ -74,7 +76,7 @@ public void emitProviderReady(ProviderEventDetails details) { * Emit a * {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED} * event. Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)} - * + * * @param details The details of the event */ public void emitProviderConfigurationChanged(ProviderEventDetails details) { @@ -84,7 +86,7 @@ public void emitProviderConfigurationChanged(ProviderEventDetails details) { /** * Emit a {@link ProviderEvent#PROVIDER_STALE} event. * Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)} - * + * * @param details The details of the event */ public void emitProviderStale(ProviderEventDetails details) { @@ -94,7 +96,7 @@ public void emitProviderStale(ProviderEventDetails details) { /** * Emit a {@link ProviderEvent#PROVIDER_ERROR} event. * Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)} - * + * * @param details The details of the event */ public void emitProviderError(ProviderEventDetails details) { diff --git a/src/main/java/dev/openfeature/sdk/EventProviderListener.java b/src/main/java/dev/openfeature/sdk/EventProviderListener.java new file mode 100644 index 000000000..c1f839aab --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/EventProviderListener.java @@ -0,0 +1,6 @@ +package dev.openfeature.sdk; + +@FunctionalInterface +interface EventProviderListener { + void onEmit(ProviderEvent event, ProviderEventDetails details); +} diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/src/main/java/dev/openfeature/sdk/FeatureProvider.java index e1e06d0ab..706818e85 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProvider.java @@ -60,13 +60,25 @@ default void shutdown() { * If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}. * If the provider is in an error state, it should return {@link ProviderState#ERROR}. * If the provider is functioning normally, it should return {@link ProviderState#READY}. - * + * *

Providers which do not implement this method are assumed to be ready immediately.

- * + * * @return ProviderState + * @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead. */ + @Deprecated default ProviderState getState() { return ProviderState.READY; } + /** + * Feature provider implementations can opt in for to support Tracking by implementing this method. + * + * @param eventName The name of the tracking event + * @param context Evaluation context used in flag evaluation (Optional) + * @param details Data pertinent to a particular tracking event (Optional) + */ + default void track(String eventName, EvaluationContext context, TrackingEventDetails details) { + + } } diff --git a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java new file mode 100644 index 000000000..973d46997 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java @@ -0,0 +1,71 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.exceptions.OpenFeatureError; +import lombok.Getter; + +import java.util.concurrent.atomic.AtomicBoolean; + +class FeatureProviderStateManager implements EventProviderListener { + private final FeatureProvider delegate; + private final AtomicBoolean isInitialized = new AtomicBoolean(); + @Getter + private ProviderState state = ProviderState.NOT_READY; + + public FeatureProviderStateManager(FeatureProvider delegate) { + this.delegate = delegate; + if (delegate instanceof EventProvider) { + ((EventProvider) delegate).setEventProviderListener(this); + } + } + + public void initialize(EvaluationContext evaluationContext) throws Exception { + if (isInitialized.getAndSet(true)) { + return; + } + try { + delegate.initialize(evaluationContext); + state = ProviderState.READY; + } catch (OpenFeatureError openFeatureError) { + if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) { + state = ProviderState.FATAL; + } else { + state = ProviderState.ERROR; + } + isInitialized.set(false); + throw openFeatureError; + } catch (Exception e) { + state = ProviderState.ERROR; + isInitialized.set(false); + throw e; + } + } + + public void shutdown() { + delegate.shutdown(); + state = ProviderState.NOT_READY; + isInitialized.set(false); + } + + @Override + public void onEmit(ProviderEvent event, ProviderEventDetails details) { + if (ProviderEvent.PROVIDER_ERROR.equals(event)) { + if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) { + state = ProviderState.FATAL; + } else { + state = ProviderState.ERROR; + } + } else if (ProviderEvent.PROVIDER_STALE.equals(event)) { + state = ProviderState.STALE; + } else if (ProviderEvent.PROVIDER_READY.equals(event)) { + state = ProviderState.READY; + } + } + + FeatureProvider getProvider() { + return delegate; + } + + public boolean hasSameProvider(FeatureProvider featureProvider) { + return this.delegate.equals(featureProvider); + } +} diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index 52c5b9727..f0216b255 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -1,13 +1,11 @@ package dev.openfeature.sdk; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,11 +17,7 @@ class HookSupport { public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List hooks, Map hints) { - Stream result = callBeforeHooks(flagValueType, hookCtx, hooks, hints); - return hookCtx.getCtx().merge( - result.reduce(hookCtx.getCtx(), (EvaluationContext accumulated, EvaluationContext current) -> { - return accumulated.merge(current); - })); + return callBeforeHooks(flagValueType, hookCtx, hooks, hints); } public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details, @@ -46,10 +40,11 @@ private void executeHooks( String hookMethod, Consumer> hookCode) { if (hooks != null) { - hooks - .stream() - .filter(hook -> hook.supportsFlagValueType(flagValueType)) - .forEach(hook -> executeChecked(hook, hookCode, hookMethod)); + for (Hook hook : hooks) { + if (hook.supportsFlagValueType(flagValueType)) { + executeChecked(hook, hookCode, hookMethod); + } + } } } @@ -68,29 +63,29 @@ private void executeHooksUnchecked( FlagValueType flagValueType, List hooks, Consumer> hookCode) { if (hooks != null) { - hooks - .stream() - .filter(hook -> hook.supportsFlagValueType(flagValueType)) - .forEach(hookCode::accept); + for (Hook hook : hooks) { + if (hook.supportsFlagValueType(flagValueType)) { + hookCode.accept(hook); + } + } } } - private Stream callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx, + private EvaluationContext callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx, List hooks, Map hints) { // These traverse backwards from normal. - List reversedHooks = IntStream - .range(0, hooks.size()) - .map(i -> hooks.size() - 1 - i) - .mapToObj(hooks::get) - .collect(Collectors.toList()); - - return reversedHooks - .stream() - .filter(hook -> hook.supportsFlagValueType(flagValueType)) - .map(hook -> hook.before(hookCtx, hints)) - .filter(Objects::nonNull) - .filter(Optional::isPresent) - .map(Optional::get) - .map(EvaluationContext.class::cast); + 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()); + } + } + } + return context; } } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index fd2ff2a68..d0dae6051 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Function; + import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; import lombok.ToString; import lombok.experimental.Delegate; @@ -10,7 +11,8 @@ /** * The EvaluationContext is a container for arbitrary contextual data * that can be used as a basis for dynamic evaluation. - * The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can + * The ImmutableContext is an EvaluationContext implementation which is + * threadsafe, and whose attributes can * not be modified after instantiation. */ @ToString @@ -21,7 +23,8 @@ public final class ImmutableContext implements EvaluationContext { private final ImmutableStructure structure; /** - * Create an immutable context with an empty targeting_key and attributes provided. + * Create an immutable context with an empty targeting_key and attributes + * provided. */ public ImmutableContext() { this(new HashMap<>()); @@ -42,7 +45,7 @@ public ImmutableContext(String targetingKey) { * @param attributes evaluation context attributes */ public ImmutableContext(Map attributes) { - this("", attributes); + this(null, attributes); } /** @@ -53,9 +56,7 @@ public ImmutableContext(Map attributes) { */ public ImmutableContext(String targetingKey, Map attributes) { if (targetingKey != null && !targetingKey.trim().isEmpty()) { - final Map actualAttribs = new HashMap<>(attributes); - actualAttribs.put(TARGETING_KEY, new Value(targetingKey)); - this.structure = new ImmutableStructure(actualAttribs); + this.structure = new ImmutableStructure(targetingKey, attributes); } else { this.structure = new ImmutableStructure(attributes); } @@ -71,28 +72,33 @@ public String getTargetingKey() { } /** - * Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict. + * Merges this EvaluationContext object with the passed EvaluationContext, + * overriding in case of conflict. * * @param overridingContext overriding context * @return new, resulting merged context */ @Override public EvaluationContext merge(EvaluationContext overridingContext) { - if (overridingContext == null) { - return new ImmutableContext(this.asMap()); + if (overridingContext == null || overridingContext.isEmpty()) { + return new ImmutableContext(this.asUnmodifiableMap()); + } + if (this.isEmpty()) { + return new ImmutableContext(overridingContext.asUnmodifiableMap()); } - return new ImmutableContext( - this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap())); + Map attributes = this.asMap(); + EvaluationContext.mergeMaps(ImmutableStructure::new, attributes, + overridingContext.asUnmodifiableMap()); + return new ImmutableContext(attributes); } @SuppressWarnings("all") private static class DelegateExclusions { @ExcludeFromGeneratedCoverageReport - public Map merge(Function, Structure> newStructure, + public Map merge(Function, Structure> newStructure, Map base, Map overriding) { - return null; } } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index d70a01637..06c2551ff 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; @@ -35,14 +36,11 @@ public ImmutableStructure() { * @param attributes attributes. */ public ImmutableStructure(Map attributes) { - super(new HashMap<>(attributes.entrySet() - .stream() - .collect(HashMap::new, - (accumulated, entry) -> accumulated.put(entry.getKey(), - Optional.ofNullable(entry.getValue()) - .map(Value::clone) - .orElse(null)), - HashMap::putAll))); + super(copyAttributes(attributes, null)); + } + + protected ImmutableStructure(String targetingKey, Map attributes) { + super(copyAttributes(attributes, targetingKey)); } @Override @@ -53,7 +51,7 @@ public Set keySet() { // getters @Override public Value getValue(String key) { - Value value = this.attributes.get(key); + Value value = attributes.get(key); return value != null ? value.clone() : null; } @@ -64,14 +62,23 @@ public Value getValue(String key) { */ @Override public Map asMap() { - return attributes - .entrySet() - .stream() - .collect(HashMap::new, - (accumulated, entry) -> accumulated.put(entry.getKey(), - Optional.ofNullable(entry.getValue()) - .map(Value::clone) - .orElse(null)), - HashMap::putAll); + return copyAttributes(attributes); + } + + private static Map copyAttributes(Map in) { + return copyAttributes(in, null); } + + 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 (targetingKey != null) { + copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey)); + } + return copy; + } + } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java new file mode 100644 index 000000000..b535bb7da --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java @@ -0,0 +1,53 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; +import lombok.experimental.Delegate; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + + +/** + * ImmutableTrackingEventDetails represents data pertinent to a particular tracking event. + */ +public class ImmutableTrackingEventDetails implements TrackingEventDetails { + + @Delegate(excludes = DelegateExclusions.class) + private final ImmutableStructure structure; + + private final Number value; + + public ImmutableTrackingEventDetails() { + this.value = null; + this.structure = new ImmutableStructure(); + } + + public ImmutableTrackingEventDetails(final Number value) { + this.value = value; + this.structure = new ImmutableStructure(); + } + + public ImmutableTrackingEventDetails(final Number value, final Map attributes) { + this.value = value; + this.structure = new ImmutableStructure(attributes); + } + + /** + * Returns the optional tracking value. + */ + public Optional getValue() { + return Optional.ofNullable(value); + } + + + @SuppressWarnings("all") + private static class DelegateExclusions { + @ExcludeFromGeneratedCoverageReport + public Map merge(Function, Structure> newStructure, + Map base, + Map overriding) { + return null; + } + } +} diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java index 653441d31..ffab28af2 100644 --- a/src/main/java/dev/openfeature/sdk/MutableContext.java +++ b/src/main/java/dev/openfeature/sdk/MutableContext.java @@ -33,7 +33,7 @@ public MutableContext(String targetingKey) { } public MutableContext(Map attributes) { - this("", attributes); + this(null, new HashMap<>(attributes)); } /** @@ -44,7 +44,7 @@ public MutableContext(Map attributes) { * @param attributes evaluation context attributes */ public MutableContext(String targetingKey, Map attributes) { - this.structure = new MutableStructure(attributes); + this.structure = new MutableStructure(new HashMap<>(attributes)); if (targetingKey != null && !targetingKey.trim().isEmpty()) { this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey)); } @@ -114,13 +114,17 @@ public String getTargetingKey() { */ @Override public EvaluationContext merge(EvaluationContext overridingContext) { - if (overridingContext == null) { - return new MutableContext(this.asMap()); + if (overridingContext == null || overridingContext.isEmpty()) { + return this; + } + if (this.isEmpty()) { + return overridingContext; } - Map merged = this.merge( - MutableStructure::new, this.asMap(), overridingContext.asMap()); - return new MutableContext(merged); + Map attributes = this.asMap(); + EvaluationContext.mergeMaps( + MutableStructure::new, attributes, overridingContext.asUnmodifiableMap()); + return new MutableContext(attributes); } /** diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index fadd68051..1246aa5ef 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -30,13 +30,13 @@ public MutableStructure(Map attributes) { @Override public Set keySet() { - return this.attributes.keySet(); + return attributes.keySet(); } // getters @Override public Value getValue(String key) { - return this.attributes.get(key); + return attributes.get(key); } // adders @@ -87,6 +87,6 @@ public MutableStructure add(String key, List value) { */ @Override public Map asMap() { - return new HashMap<>(this.attributes); + return new HashMap<>(attributes); } } diff --git a/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java new file mode 100644 index 000000000..9f0de8c3a --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java @@ -0,0 +1,94 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.experimental.Delegate; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * MutableTrackingEventDetails represents data pertinent to a particular tracking event. + */ +@EqualsAndHashCode +@ToString +public class MutableTrackingEventDetails implements TrackingEventDetails { + + private final Number value; + @Delegate(excludes = MutableTrackingEventDetails.DelegateExclusions.class) + private final MutableStructure structure; + + public MutableTrackingEventDetails() { + this.value = null; + this.structure = new MutableStructure(); + } + + public MutableTrackingEventDetails(final Number value) { + this.value = value; + this.structure = new MutableStructure(); + } + + /** + * Returns the optional tracking value. + */ + public Optional getValue() { + return Optional.ofNullable(value); + } + + // override @Delegate methods so that we can use "add" methods and still return MutableTrackingEventDetails, + // not Structure + public MutableTrackingEventDetails add(String key, Boolean value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, String value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, Integer value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, Double value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, Instant value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, Structure value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, List value) { + this.structure.add(key, value); + return this; + } + + public MutableTrackingEventDetails add(String key, Value value) { + this.structure.add(key, value); + return this; + } + + + @SuppressWarnings("all") + private static class DelegateExclusions { + @ExcludeFromGeneratedCoverageReport + public Map merge(Function, Structure> newStructure, + Map base, + Map overriding) { + return null; + } + } +} diff --git a/src/main/java/dev/openfeature/sdk/NoOpProvider.java b/src/main/java/dev/openfeature/sdk/NoOpProvider.java index ef8cf1f83..2ad59c8bf 100644 --- a/src/main/java/dev/openfeature/sdk/NoOpProvider.java +++ b/src/main/java/dev/openfeature/sdk/NoOpProvider.java @@ -59,7 +59,7 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, - EvaluationContext invocationContext) { + EvaluationContext invocationContext) { return ProviderEvaluation.builder() .value(defaultValue) .variant(PASSED_IN_DEFAULT) diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index f4d20caf2..ad528568a 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -5,11 +5,7 @@ import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; /** @@ -69,7 +65,7 @@ public Metadata getProviderMetadata(String domain) { } /** - * A factory function for creating new, OpenFeature clients. + * A factory function for creating new, OpenFeature client. * Clients can contain their own state (e.g. logger, hook, context). * Multiple clients can be used to segment feature flag configuration. * All un-named or unbound clients use the default provider. @@ -81,12 +77,12 @@ public Client getClient() { } /** - * A factory function for creating new domainless OpenFeature clients. + * A factory function for creating new domainless OpenFeature client. * Clients can contain their own state (e.g. logger, hook, context). * Multiple clients can be used to segment feature flag configuration. * If there is already a provider bound to this domain, this provider will be used. * Otherwise, the default provider is used until a provider is assigned to that domain. - * + * * @param domain an identifier which logically binds clients with providers * @return a new client instance */ @@ -95,20 +91,22 @@ public Client getClient(String domain) { } /** - * A factory function for creating new domainless OpenFeature clients. + * A factory function for creating new domainless OpenFeature client. * Clients can contain their own state (e.g. logger, hook, context). * Multiple clients can be used to segment feature flag configuration. * If there is already a provider bound to this domain, this provider will be used. * Otherwise, the default provider is used until a provider is assigned to that domain. - * - * @param domain a identifier which logically binds clients with providers + * + * @param domain a identifier which logically binds clients with providers * @param version a version identifier * @return a new client instance */ public Client getClient(String domain, String version) { - return new OpenFeatureClient(this, + return new OpenFeatureClient( + this, domain, - version); + version + ); } /** @@ -193,8 +191,8 @@ public void setProvider(FeatureProvider provider) { /** * Add a provider for a domain. * - * @param domain The domain to bind the provider to. - * @param provider The provider to set. + * @param domain The domain to bind the provider to. + * @param provider The provider to set. */ public void setProvider(String domain, FeatureProvider provider) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { @@ -226,8 +224,8 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError /** * Add a provider for a domain and wait for initialization to finish. * - * @param domain The domain to bind the provider to. - * @param provider The provider to set. + * @param domain The domain to bind the provider to. + * @param provider The provider to set. */ public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { @@ -300,6 +298,7 @@ public void addHooks(Hook... hooks) { /** * Fetch the hooks associated to this client. + * * @return A list of {@link Hook}s. */ public List getHooks() { @@ -394,7 +393,7 @@ void removeHandler(String domain, ProviderEvent event, Consumer ha void addHandler(String domain, ProviderEvent event, Consumer handler) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { // if the provider is in the state associated with event, run immediately - if (Optional.ofNullable(this.providerRepository.getProvider(domain).getState()) + if (Optional.ofNullable(this.providerRepository.getProviderState(domain)) .orElse(ProviderState.READY).matchesEvent(event)) { eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build()); } @@ -402,9 +401,13 @@ void addHandler(String domain, ProviderEvent event, Consumer handl } } + FeatureProviderStateManager getFeatureProviderStateManager(String domain) { + return providerRepository.getFeatureProviderStateManager(domain); + } + /** * Runs the handlers associated with a particular provider. - * + * * @param provider the provider from where this event originated * @param event the event type * @param details the event details diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index d8004e5de..ea566e652 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -1,32 +1,36 @@ package dev.openfeature.sdk; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - +import dev.openfeature.sdk.exceptions.ExceptionUtils; +import dev.openfeature.sdk.exceptions.FatalError; 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.exceptions.ExceptionUtils; import dev.openfeature.sdk.internal.ObjectUtils; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + /** * OpenFeature Client implementation. * You should not instantiate this or reference this class. * Use the dev.openfeature.sdk.Client interface instead. + * * @see Client - * * @deprecated // TODO: eventually we will make this non-public. See issue #872 */ @Slf4j -@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", - "PMD.UnusedLocalVariable", "unchecked", "rawtypes" }) +@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "PMD.UnusedLocalVariable", + "unchecked", "rawtypes"}) @Deprecated() // TODO: eventually we will make this non-public. See issue #872 public class OpenFeatureClient implements Client { @@ -45,14 +49,18 @@ public class OpenFeatureClient implements Client { * Deprecated public constructor. Use OpenFeature.API.getClient() instead. * * @param openFeatureAPI Backing global singleton - * @param domain An identifier which logically binds clients with providers (used by observability tools). + * @param domain An identifier which logically binds clients with + * providers (used by observability tools). * @param version Version of the client (used by observability tools). * @deprecated Do not use this constructor. It's for internal use only. * Clients created using it will not run event handlers. * Use the OpenFeatureAPI's getClient factory method instead. */ @Deprecated() // TODO: eventually we will make this non-public. See issue #872 - public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String version) { + public OpenFeatureClient( + OpenFeatureAPI openFeatureAPI, + String domain, + String version) { this.openfeatureApi = openFeatureAPI; this.domain = domain; this.version = version; @@ -60,6 +68,56 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String ve this.hookSupport = new HookSupport(); } + /** + * {@inheritDoc} + */ + @Override + public ProviderState getProviderState() { + return openfeatureApi.getFeatureProviderStateManager(domain).getState(); + } + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName) { + validateTrackingEventName(trackingEventName); + invokeTrack(trackingEventName, null, null); + } + + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, EvaluationContext context) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(context); + invokeTrack(trackingEventName, context, null); + } + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, TrackingEventDetails details) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(details); + invokeTrack(trackingEventName, null, details); + } + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(context); + Objects.requireNonNull(details); + invokeTrack(trackingEventName, mergeEvaluationContext(context), details); + } + + /** * {@inheritDoc} */ @@ -103,11 +161,10 @@ public EvaluationContext getEvaluationContext() { } private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key, T defaultValue, - EvaluationContext ctx, FlagEvaluationOptions options) { + EvaluationContext ctx, FlagEvaluationOptions options) { FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options, () -> FlagEvaluationOptions.builder().build()); Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); - ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext()); FlagEvaluationDetails details = null; List mergedHooks = null; @@ -115,8 +172,16 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key FeatureProvider provider; try { - // openfeatureApi.getProvider() must be called once to maintain a consistent reference - provider = openfeatureApi.getProvider(this.domain); + FeatureProviderStateManager stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); + // provider must be accessed once to maintain a consistent reference + provider = stateManager.getProvider(); + ProviderState state = stateManager.getState(); + if (ProviderState.NOT_READY.equals(state)) { + throw new ProviderNotReadyError("provider not yet initialized"); + } + if (ProviderState.FATAL.equals(state)) { + throw new FatalError("provider is in an irrecoverable error state"); + } mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); @@ -132,7 +197,11 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key details = FlagEvaluationDetails.from(providerEval, key); if (details.getErrorCode() != null) { - throw ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage()); + OpenFeatureError error = ExceptionUtils.instantiateErrorByErrorCode( + details.getErrorCode(), + details.getErrorMessage()); + enrichDetailsWithErrorDefaults(defaultValue, details); + hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints); } else { hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints); } @@ -146,8 +215,7 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key details.setErrorCode(ErrorCode.GENERAL); } details.setErrorMessage(e.getMessage()); - details.setValue(defaultValue); - details.setReason(Reason.ERROR.toString()); + enrichDetailsWithErrorDefaults(defaultValue, details); hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints); } finally { hookSupport.afterAllHooks(type, afterHookContext, mergedHooks, hints); @@ -156,6 +224,24 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key return details; } + private static void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvaluationDetails details) { + details.setValue(defaultValue); + details.setReason(Reason.ERROR.toString()); + } + + private static void validateTrackingEventName(String str) { + Objects.requireNonNull(str); + if (str.isEmpty()) { + throw new IllegalArgumentException("trackingEventName cannot be empty"); + } + } + + private void invokeTrack(String trackingEventName, EvaluationContext context, TrackingEventDetails details) { + openfeatureApi.getFeatureProviderStateManager(domain) + .getProvider() + .track(trackingEventName, mergeEvaluationContext(context), details); + } + /** * Merge invocation contexts with API, transaction and client contexts. * Does not merge before context. @@ -164,17 +250,23 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key * @return merged evaluation context */ private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) { - final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null - ? openfeatureApi.getEvaluationContext() - : new ImmutableContext(); - final EvaluationContext clientContext = this.getEvaluationContext() != null - ? this.getEvaluationContext() - : new ImmutableContext(); - final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null - ? openfeatureApi.getTransactionContext() - : new ImmutableContext(); - - return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext))); + final EvaluationContext apiContext = openfeatureApi.getEvaluationContext(); + final EvaluationContext clientContext = this.getEvaluationContext(); + final EvaluationContext transactionContext = openfeatureApi.getTransactionContext(); + return mergeContextMaps(apiContext, transactionContext, clientContext, invocationContext); + } + + private EvaluationContext mergeContextMaps(EvaluationContext... contexts) { + // avoid any unnecessary context instantiations and stream usage here; this is + // called with every evaluation. + Map merged = new HashMap<>(); + for (EvaluationContext evaluationContext : contexts) { + if (evaluationContext != null && !evaluationContext.isEmpty()) { + EvaluationContext.mergeMaps(ImmutableStructure::new, merged, + evaluationContext.asUnmodifiableMap()); + } + } + return new ImmutableContext(merged); } private ProviderEvaluation createProviderEvaluation( @@ -211,7 +303,7 @@ public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationConte @Override public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getBooleanDetails(key, defaultValue, ctx, options).getValue(); } @@ -227,7 +319,7 @@ public FlagEvaluationDetails getBooleanDetails(String key, Boolean defa @Override public FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options); } @@ -243,7 +335,7 @@ public String getStringValue(String key, String defaultValue, EvaluationContext @Override public String getStringValue(String key, String defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getStringDetails(key, defaultValue, ctx, options).getValue(); } @@ -259,7 +351,7 @@ public FlagEvaluationDetails getStringDetails(String key, String default @Override public FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options); } @@ -275,7 +367,7 @@ public Integer getIntegerValue(String key, Integer defaultValue, EvaluationConte @Override public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getIntegerDetails(key, defaultValue, ctx, options).getValue(); } @@ -291,7 +383,7 @@ public FlagEvaluationDetails getIntegerDetails(String key, Integer defa @Override public FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options); } @@ -307,7 +399,7 @@ public Double getDoubleValue(String key, Double defaultValue, EvaluationContext @Override public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue(); } @@ -323,7 +415,7 @@ public FlagEvaluationDetails getDoubleDetails(String key, Double default @Override public FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options); } @@ -339,7 +431,7 @@ public Value getObjectValue(String key, Value defaultValue, EvaluationContext ct @Override public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getObjectDetails(key, defaultValue, ctx, options).getValue(); } @@ -350,13 +442,13 @@ public FlagEvaluationDetails getObjectDetails(String key, Value defaultVa @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options); } diff --git a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java index d28da9e50..d927e4291 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java +++ b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java @@ -13,4 +13,5 @@ public class ProviderEventDetails { private List flagsChanged; private String message; private ImmutableMetadata eventMetadata; + private ErrorCode errorCode; } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index 47b9ecf58..d3a5c1acc 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -1,5 +1,9 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.OpenFeatureError; +import lombok.extern.slf4j.Slf4j; + import java.util.List; import java.util.Map; import java.util.Optional; @@ -13,26 +17,41 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.OpenFeatureError; -import lombok.extern.slf4j.Slf4j; - @Slf4j class ProviderRepository { - private final Map providers = new ConcurrentHashMap<>(); - private final AtomicReference defaultProvider = new AtomicReference<>(new NoOpProvider()); + 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 Object registerStateManagerLock = new Object(); + + FeatureProviderStateManager getFeatureProviderStateManager() { + return defaultStateManger.get(); + } + + FeatureProviderStateManager getFeatureProviderStateManager(String domain) { + if (domain == null) { + return defaultStateManger.get(); + } + FeatureProviderStateManager fromMap = this.stateManagers.get(domain); + if (fromMap == null) { + return this.defaultStateManger.get(); + } else { + return fromMap; + } + } /** * Return the default provider. */ public FeatureProvider getProvider() { - return defaultProvider.get(); + return defaultStateManger.get().getProvider(); } /** @@ -42,17 +61,46 @@ public FeatureProvider getProvider() { * @return A named {@link FeatureProvider} */ public FeatureProvider getProvider(String domain) { - return Optional.ofNullable(domain).map(this.providers::get).orElse(this.defaultProvider.get()); + return getFeatureProviderStateManager(domain).getProvider(); + } + + public ProviderState getProviderState() { + return getFeatureProviderStateManager().getState(); + } + + public ProviderState getProviderState(FeatureProvider featureProvider) { + if (featureProvider instanceof FeatureProviderStateManager) { + return ((FeatureProviderStateManager) featureProvider).getState(); + } + + FeatureProviderStateManager defaultProvider = this.defaultStateManger.get(); + if (defaultProvider.hasSameProvider(featureProvider)) { + return defaultProvider.getState(); + } + + for (FeatureProviderStateManager wrapper : stateManagers.values()) { + if (wrapper.hasSameProvider(featureProvider)) { + return wrapper.getState(); + } + } + return null; + } + + public ProviderState getProviderState(String domain) { + return Optional.ofNullable(domain) + .map(this.stateManagers::get) + .orElse(this.defaultStateManger.get()) + .getState(); } public List getDomainsForProvider(FeatureProvider provider) { - return providers.entrySet().stream() - .filter(entry -> entry.getValue().equals(provider)) - .map(entry -> entry.getKey()).collect(Collectors.toList()); + return stateManagers.entrySet().stream() + .filter(entry -> entry.getValue().hasSameProvider(provider)) + .map(Map.Entry::getKey).collect(Collectors.toList()); } public Set getAllBoundDomains() { - return providers.keySet(); + return stateManagers.keySet(); } public boolean isDefaultProvider(FeatureProvider provider) { @@ -63,11 +111,11 @@ public boolean isDefaultProvider(FeatureProvider provider) { * Set the default provider. */ public void setProvider(FeatureProvider provider, - Consumer afterSet, - Consumer afterInit, - Consumer afterShutdown, - BiConsumer afterError, - boolean waitForInit) { + Consumer afterSet, + Consumer afterInit, + Consumer afterShutdown, + BiConsumer afterError, + boolean waitForInit) { if (provider == null) { throw new IllegalArgumentException("Provider cannot be null"); } @@ -83,12 +131,12 @@ public void setProvider(FeatureProvider provider, * Otherwise, initialization happens in the background. */ public void setProvider(String domain, - FeatureProvider provider, - Consumer afterSet, - Consumer afterInit, - Consumer afterShutdown, - BiConsumer afterError, - boolean waitForInit) { + FeatureProvider provider, + Consumer afterSet, + Consumer afterInit, + Consumer afterShutdown, + BiConsumer afterError, + boolean waitForInit) { if (provider == null) { throw new IllegalArgumentException("Provider cannot be null"); } @@ -99,68 +147,105 @@ public void setProvider(String domain, } private void prepareAndInitializeProvider(String domain, - FeatureProvider newProvider, - Consumer afterSet, - Consumer afterInit, - Consumer afterShutdown, - BiConsumer afterError, - boolean waitForInit) { - - if (!isProviderRegistered(newProvider)) { - // only run afterSet if new provider is not already attached - afterSet.accept(newProvider); - } + FeatureProvider newProvider, + Consumer afterSet, + Consumer afterInit, + Consumer afterShutdown, + BiConsumer afterError, + boolean waitForInit) { + final FeatureProviderStateManager newStateManager; + final FeatureProviderStateManager oldStateManager; + + synchronized (registerStateManagerLock) { + FeatureProviderStateManager existing = getExistingStateManagerForProvider(newProvider); + if (existing == null) { + newStateManager = new FeatureProviderStateManager(newProvider); + // only run afterSet if new provider is not already attached + afterSet.accept(newProvider); + } else { + newStateManager = existing; + } - // provider is set immediately, on this thread - FeatureProvider oldProvider = domain != null - ? this.providers.put(domain, newProvider) - : this.defaultProvider.getAndSet(newProvider); + // provider is set immediately, on this thread + oldStateManager = domain != null + ? this.stateManagers.put(domain, newStateManager) + : this.defaultStateManger.getAndSet(newStateManager); + } if (waitForInit) { - initializeProvider(newProvider, afterInit, afterShutdown, afterError, oldProvider); + initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager); } else { taskExecutor.submit(() -> { - // initialization happens in a different thread if we're not waiting it - initializeProvider(newProvider, afterInit, afterShutdown, afterError, oldProvider); + // initialization happens in a different thread if we're not waiting for it + initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager); }); } } - private void initializeProvider(FeatureProvider newProvider, - Consumer afterInit, - Consumer afterShutdown, - BiConsumer afterError, - FeatureProvider oldProvider) { + private FeatureProviderStateManager getExistingStateManagerForProvider(FeatureProvider provider) { + for (FeatureProviderStateManager stateManager : stateManagers.values()) { + if (stateManager.hasSameProvider(provider)) { + return stateManager; + } + } + FeatureProviderStateManager defaultFeatureProviderStateManager = defaultStateManger.get(); + if (defaultFeatureProviderStateManager.hasSameProvider(provider)) { + return defaultFeatureProviderStateManager; + } + return null; + } + + private void initializeProvider(FeatureProviderStateManager newManager, + Consumer afterInit, + Consumer afterShutdown, + BiConsumer afterError, + FeatureProviderStateManager oldManager) { try { - if (ProviderState.NOT_READY.equals(newProvider.getState())) { - newProvider.initialize(OpenFeatureAPI.getInstance().getEvaluationContext()); - afterInit.accept(newProvider); + if (ProviderState.NOT_READY.equals(newManager.getState())) { + newManager.initialize(OpenFeatureAPI.getInstance().getEvaluationContext()); + afterInit.accept(newManager.getProvider()); } - shutDownOld(oldProvider, afterShutdown); + shutDownOld(oldManager, afterShutdown); } catch (OpenFeatureError e) { - log.error("Exception when initializing feature provider {}", newProvider.getClass().getName(), e); - afterError.accept(newProvider, e); + log.error( + "Exception when initializing feature provider {}", + newManager.getProvider().getClass().getName(), + e + ); + afterError.accept(newManager.getProvider(), e); } catch (Exception e) { - log.error("Exception when initializing feature provider {}", newProvider.getClass().getName(), e); - afterError.accept(newProvider, new GeneralError(e)); + log.error( + "Exception when initializing feature provider {}", + newManager.getProvider().getClass().getName(), + e + ); + afterError.accept(newManager.getProvider(), new GeneralError(e)); } } - private void shutDownOld(FeatureProvider oldProvider, Consumer afterShutdown) { - if (!isProviderRegistered(oldProvider)) { - shutdownProvider(oldProvider); - afterShutdown.accept(oldProvider); + private void shutDownOld(FeatureProviderStateManager oldManager, Consumer afterShutdown) { + if (oldManager != null && !isStateManagerRegistered(oldManager)) { + shutdownProvider(oldManager); + afterShutdown.accept(oldManager.getProvider()); } } /** - * Helper to check if provider is already known (registered). - * @param provider provider to check for registration + * Helper to check if manager is already known (registered). + * + * @param manager manager to check for registration * @return boolean true if already registered, false otherwise */ - private boolean isProviderRegistered(FeatureProvider provider) { - return provider != null - && (this.providers.containsValue(provider) || this.defaultProvider.get().equals(provider)); + private boolean isStateManagerRegistered(FeatureProviderStateManager manager) { + return manager != null + && (this.stateManagers.containsValue(manager) || this.defaultStateManger.get().equals(manager)); + } + + private void shutdownProvider(FeatureProviderStateManager manager) { + if (manager == null) { + return; + } + shutdownProvider(manager.getProvider()); } private void shutdownProvider(FeatureProvider provider) { @@ -180,10 +265,10 @@ private void shutdownProvider(FeatureProvider provider) { */ public void shutdown() { Stream - .concat(Stream.of(this.defaultProvider.get()), this.providers.values().stream()) + .concat(Stream.of(this.defaultStateManger.get()), this.stateManagers.values().stream()) .distinct() .forEach(this::shutdownProvider); - this.providers.clear(); + this.stateManagers.clear(); taskExecutor.shutdown(); } } diff --git a/src/main/java/dev/openfeature/sdk/ProviderState.java b/src/main/java/dev/openfeature/sdk/ProviderState.java index a66d4e944..b924b4d22 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderState.java +++ b/src/main/java/dev/openfeature/sdk/ProviderState.java @@ -4,7 +4,7 @@ * Indicates the state of the provider. */ public enum ProviderState { - READY, NOT_READY, ERROR, STALE; + READY, NOT_READY, ERROR, STALE, FATAL; /** * Returns true if the passed ProviderEvent maps to this ProviderState. diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java index f3768e958..f2fdc53e7 100644 --- a/src/main/java/dev/openfeature/sdk/Structure.java +++ b/src/main/java/dev/openfeature/sdk/Structure.java @@ -5,8 +5,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.Map.Entry; -import java.util.function.Function; import java.util.stream.Collectors; import static dev.openfeature.sdk.Value.objectToValue; @@ -18,6 +16,12 @@ @SuppressWarnings("PMD.BeanMembersShouldSerialize") public interface Structure { + /** + * Boolean indicating if this structure is empty. + * @return boolean for emptiness + */ + boolean isEmpty(); + /** * Get all keys. * @@ -40,6 +44,14 @@ public interface Structure { */ Map asMap(); + /** + * Get all values, as a map of Values. + * + * @return all attributes on the structure into a Map + */ + Map asUnmodifiableMap(); + + /** * Get all values, with as a map of Object. * @@ -89,7 +101,7 @@ default Object convertValue(Value value) { if (value.isStructure()) { Structure s = value.asStructure(); - return s.asMap() + return s.asUnmodifiableMap() .entrySet() .stream() .collect(HashMap::new, @@ -101,34 +113,6 @@ default Object convertValue(Value value) { throw new ValueNotConvertableError(); } - /** - * Recursively merges the base map with the overriding map. - * - * @param Structure type - * @param newStructure function to create the right structure - * @param base base map to merge - * @param overriding overriding map to merge - * @return resulting merged map - */ - default Map merge(Function, Structure> newStructure, - Map base, - Map overriding) { - - final Map merged = new HashMap<>(base); - for (Entry overridingEntry : overriding.entrySet()) { - String key = overridingEntry.getKey(); - if (overridingEntry.getValue().isStructure() && merged.containsKey(key) && merged.get(key).isStructure()) { - Structure mergedValue = merged.get(key).asStructure(); - Structure overridingValue = overridingEntry.getValue().asStructure(); - Map newMap = this.merge(newStructure, mergedValue.asMap(), overridingValue.asMap()); - merged.put(key, new Value(newStructure.apply(newMap))); - } else { - merged.put(key, overridingEntry.getValue()); - } - } - return merged; - } - /** * Transform an object map to a {@link Structure} type. * diff --git a/src/main/java/dev/openfeature/sdk/Tracking.java b/src/main/java/dev/openfeature/sdk/Tracking.java new file mode 100644 index 000000000..ec9c8a8fe --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/Tracking.java @@ -0,0 +1,42 @@ +package dev.openfeature.sdk; + +/** + * Interface for Tracking events. + */ +public interface Tracking { + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param context Evaluation context used in flag evaluation + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, EvaluationContext context); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param details Data pertinent to a particular tracking event + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, TrackingEventDetails details); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param context Evaluation context used in flag evaluation + * @param details Data pertinent to a particular tracking event + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details); +} diff --git a/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java new file mode 100644 index 000000000..76b20fbbf --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java @@ -0,0 +1,7 @@ +package dev.openfeature.sdk; + +/** + * Data pertinent to a particular tracking event. + */ +public interface TrackingEventDetails extends Structure { +} diff --git a/src/main/java/dev/openfeature/sdk/Value.java b/src/main/java/dev/openfeature/sdk/Value.java index f0fdc8d45..7464ce5af 100644 --- a/src/main/java/dev/openfeature/sdk/Value.java +++ b/src/main/java/dev/openfeature/sdk/Value.java @@ -274,7 +274,7 @@ protected Value clone() { return new Value(copy); } if (this.isStructure()) { - return new Value(new ImmutableStructure(this.asStructure().asMap())); + return new Value(new ImmutableStructure(this.asStructure().asUnmodifiableMap())); } if (this.isInstant()) { Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli()); diff --git a/src/main/java/dev/openfeature/sdk/exceptions/FatalError.java b/src/main/java/dev/openfeature/sdk/exceptions/FatalError.java new file mode 100644 index 000000000..d50d1a42c --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/exceptions/FatalError.java @@ -0,0 +1,13 @@ +package dev.openfeature.sdk.exceptions; + +import dev.openfeature.sdk.ErrorCode; +import lombok.Getter; +import lombok.experimental.StandardException; + +@SuppressWarnings("checkstyle:MissingJavadocType") +@StandardException +public class FatalError extends OpenFeatureError { + private static final long serialVersionUID = 1L; + @Getter + private final ErrorCode errorCode = ErrorCode.PROVIDER_FATAL; +} \ No newline at end of file diff --git a/src/main/java/dev/openfeature/sdk/exceptions/FlagNotFoundError.java b/src/main/java/dev/openfeature/sdk/exceptions/FlagNotFoundError.java index 6685f96d5..7c88ebb44 100644 --- a/src/main/java/dev/openfeature/sdk/exceptions/FlagNotFoundError.java +++ b/src/main/java/dev/openfeature/sdk/exceptions/FlagNotFoundError.java @@ -4,14 +4,11 @@ import lombok.Getter; import lombok.experimental.StandardException; -@SuppressWarnings("checkstyle:MissingJavadocType") +@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"}) @StandardException -public class FlagNotFoundError extends OpenFeatureError { +public class FlagNotFoundError extends OpenFeatureErrorWithoutStacktrace { private static final long serialVersionUID = 1L; - @Getter private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND; + @Getter + private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND; - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } } diff --git a/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureErrorWithoutStacktrace.java b/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureErrorWithoutStacktrace.java new file mode 100644 index 000000000..2931e6bbb --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureErrorWithoutStacktrace.java @@ -0,0 +1,14 @@ +package dev.openfeature.sdk.exceptions; + +import lombok.experimental.StandardException; + +@SuppressWarnings("checkstyle:MissingJavadocType") +@StandardException +public abstract class OpenFeatureErrorWithoutStacktrace extends OpenFeatureError { + private static final long serialVersionUID = 1L; + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java b/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java index 218073441..0416eae26 100644 --- a/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java +++ b/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java @@ -4,9 +4,9 @@ import lombok.Getter; import lombok.experimental.StandardException; -@SuppressWarnings("checkstyle:MissingJavadocType") +@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"}) @StandardException -public class ProviderNotReadyError extends OpenFeatureError { +public class ProviderNotReadyError extends OpenFeatureErrorWithoutStacktrace { private static final long serialVersionUID = 1L; @Getter private final ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; } diff --git a/src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java b/src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java index d27c6209f..9d88922c7 100644 --- a/src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java +++ b/src/main/java/dev/openfeature/sdk/exceptions/TypeMismatchError.java @@ -7,6 +7,7 @@ /** * The type of the flag value does not match the expected type. */ +@SuppressWarnings({"checkstyle:MissingJavadocType", "squid:S110"}) @StandardException public class TypeMismatchError extends OpenFeatureError { private static final long serialVersionUID = 1L; diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java index 34caadaea..9e5dcf613 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java +++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java @@ -1,11 +1,9 @@ package dev.openfeature.sdk.internal; -import java.util.Arrays; -import java.util.Collection; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Supplier; -import java.util.stream.Collectors; import lombok.experimental.UtilityClass; @@ -64,9 +62,10 @@ public static T defaultIfNull(T source, Supplier defaultValue) { */ @SafeVarargs public static List merge(List... sources) { - return Arrays - .stream(sources) - .flatMap(Collection::stream) - .collect(Collectors.toList()); + List merged = new ArrayList<>(); + for (List source : sources) { + merged.addAll(source); + } + return merged; } } 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 5b6d28702..2bea3e4ef 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -1,18 +1,7 @@ package dev.openfeature.sdk.providers.memory; -import dev.openfeature.sdk.EvaluationContext; -import dev.openfeature.sdk.EventProvider; -import dev.openfeature.sdk.Metadata; -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.exceptions.FlagNotFoundError; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.OpenFeatureError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; -import dev.openfeature.sdk.exceptions.TypeMismatchError; +import dev.openfeature.sdk.*; +import dev.openfeature.sdk.exceptions.*; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -63,6 +52,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { * Updates the provider flags configuration. * For existing flags, the new configurations replace the old one. * For new flags, they are added to the configuration. + * * @param newFlags the new flag configurations */ public void updateFlags(Map> newFlags) { @@ -80,6 +70,7 @@ public void updateFlags(Map> newFlags) { * Updates a single provider flag configuration. * For existing flag, the new configuration replaces the old one. * For new flag, they are added to the configuration. + * * @param newFlag the flag to update */ public void updateFlag(String flagKey, Flag newFlag) { @@ -93,32 +84,32 @@ public void updateFlag(String flagKey, Flag newFlag) { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, - EvaluationContext evaluationContext) { + EvaluationContext evaluationContext) { return getEvaluation(key, evaluationContext, Boolean.class); } @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, - EvaluationContext evaluationContext) { + EvaluationContext evaluationContext) { return getEvaluation(key, evaluationContext, String.class); } @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, - EvaluationContext evaluationContext) { + EvaluationContext evaluationContext) { return getEvaluation(key, evaluationContext, Integer.class); } @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, - EvaluationContext evaluationContext) { + EvaluationContext evaluationContext) { return getEvaluation(key, evaluationContext, Double.class); } @SneakyThrows @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, - EvaluationContext evaluationContext) { + EvaluationContext evaluationContext) { return getEvaluation(key, evaluationContext, Value.class); } @@ -129,6 +120,9 @@ private ProviderEvaluation getEvaluation( if (ProviderState.NOT_READY.equals(state)) { throw new ProviderNotReadyError("provider not yet initialized"); } + if (ProviderState.FATAL.equals(state)) { + throw new FatalError("provider in fatal error state"); + } throw new GeneralError("unknown error"); } Flag flag = flags.get(key); @@ -149,5 +143,4 @@ private ProviderEvaluation getEvaluation( .reason(Reason.STATIC.toString()) .build(); } - } diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java index c10976232..841d738e5 100644 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java @@ -4,11 +4,11 @@ public class AlwaysBrokenProvider implements FeatureProvider { + private final String name = "always broken"; + @Override public Metadata getMetadata() { - return () -> { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - }; + return () -> name; } @Override diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java new file mode 100644 index 000000000..b3ead41bd --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java @@ -0,0 +1,53 @@ +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/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java index b5e5bedf3..4502699b1 100644 --- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java +++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java @@ -1,38 +1,38 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.fixtures.HookFixtures; +import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import dev.openfeature.sdk.testutils.TestEventsProvider; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; -import org.junit.jupiter.api.Test; - -import dev.openfeature.sdk.fixtures.HookFixtures; - class DeveloperExperienceTest implements HookFixtures { transient String flagKey = "mykey"; - @Test void simpleBooleanFlag() { + @Test + void simpleBooleanFlag() { OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new NoOpProvider()); + api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Boolean retval = client.getBooleanValue(flagKey, false); assertFalse(retval); } - @Test void clientHooks() { + @Test + void clientHooks() { Hook exampleHook = mockBooleanHook(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new NoOpProvider()); + api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(exampleHook); Boolean retval = client.getBooleanValue(flagKey, false); @@ -40,12 +40,13 @@ class DeveloperExperienceTest implements HookFixtures { assertFalse(retval); } - @Test void evalHooks() { + @Test + void evalHooks() { Hook clientHook = mockBooleanHook(); Hook evalHook = mockBooleanHook(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new NoOpProvider()); + api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(clientHook); Boolean retval = client.getBooleanValue(flagKey, false, null, @@ -59,10 +60,11 @@ class DeveloperExperienceTest implements HookFixtures { * As an application author, you probably know special things about your users. You can communicate these to the * provider via {@link MutableContext} */ - @Test void providingContext() { + @Test + void providingContext() { OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new NoOpProvider()); + api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Map attributes = new HashMap<>(); List values = Arrays.asList(new Value(2), new Value(4)); @@ -76,7 +78,8 @@ class DeveloperExperienceTest implements HookFixtures { assertFalse(retval); } - @Test void brokenProvider() { + @Test + void brokenProvider() { OpenFeatureAPI api = OpenFeatureAPI.getInstance(); FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client client = api.getClient(); @@ -93,13 +96,16 @@ void providerLockedPerTransaction() { class MutatingHook implements Hook { @Override + @SneakyThrows // change the provider during a before hook - this should not impact the evaluation in progress public Optional before(HookContext ctx, Map hints) { - FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); + + FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + return Optional.empty(); } } - + final String defaultValue = "string-value"; final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); @@ -111,9 +117,59 @@ public Optional before(HookContext ctx, Map hints) { assertEquals(new StringBuilder(defaultValue).reverse().toString(), doSomethingValue); api.clearHooks(); - + // subsequent evaluations should now use new provider set by hook String noOpValue = client.getStringValue("val", defaultValue); assertEquals(noOpValue, defaultValue); } + + @Test + void setProviderAndWaitShouldPutTheProviderInReadyState() { + String domain = "domain"; + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait(domain, new TestEventsProvider()); + Client client = api.getClient(domain); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { + String domain = "domain"; + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + TestEventsProvider provider = new TestEventsProvider(); + api.setProviderAndWait(domain, provider); + Client client = api.getClient(domain); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); + provider.emitProviderError(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { + String domain = "domain"; + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + TestEventsProvider provider = new TestEventsProvider(); + api.setProviderAndWait(domain, provider); + Client client = api.getClient(domain); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); + provider.emitProviderStale(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() { + String domain = "domain"; + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + TestEventsProvider provider = new TestEventsProvider(); + api.setProviderAndWait(domain, provider); + Client client = api.getClient(domain); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); + provider.emitProviderStale(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); + provider.emitProviderReady(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); + } } diff --git a/src/test/java/dev/openfeature/sdk/EventProviderTest.java b/src/test/java/dev/openfeature/sdk/EventProviderTest.java index a5be8589c..acf2ce6b3 100644 --- a/src/test/java/dev/openfeature/sdk/EventProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/EventProviderTest.java @@ -1,23 +1,34 @@ package dev.openfeature.sdk; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.internal.TriConsumer; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import dev.openfeature.sdk.internal.TriConsumer; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; class EventProviderTest { + private TestEventProvider eventProvider; + + @BeforeEach + @SneakyThrows + void setup() { + eventProvider = new TestEventProvider(); + eventProvider.initialize(null); + } + @Test @DisplayName("should run attached onEmit with emitters") void emitsEventsWhenAttached() { - TestEventProvider eventProvider = new TestEventProvider(); TriConsumer onEmit = mockOnEmit(); eventProvider.attach(onEmit); @@ -37,8 +48,6 @@ void emitsEventsWhenAttached() { @Test @DisplayName("should do nothing with emitters if no onEmit attached") void doesNotEmitsEventsWhenNotAttached() { - TestEventProvider eventProvider = new TestEventProvider(); - // don't attach this emitter TriConsumer onEmit = mockOnEmit(); @@ -56,7 +65,6 @@ void doesNotEmitsEventsWhenNotAttached() { @Test @DisplayName("should throw if second different onEmit attached") void throwsWhenOnEmitDifferent() { - TestEventProvider eventProvider = new TestEventProvider(); TriConsumer onEmit1 = mockOnEmit(); TriConsumer onEmit2 = mockOnEmit(); eventProvider.attach(onEmit1); @@ -67,15 +75,13 @@ void throwsWhenOnEmitDifferent() { @Test @DisplayName("should not throw if second same onEmit attached") void doesNotThrowWhenOnEmitSame() { - TestEventProvider eventProvider = new TestEventProvider(); TriConsumer onEmit1 = mockOnEmit(); TriConsumer onEmit2 = onEmit1; eventProvider.attach(onEmit1); eventProvider.attach(onEmit2); // should not throw, same instance. noop } - - class TestEventProvider extends EventProvider { + static class TestEventProvider extends EventProvider { private static final String NAME = "TestEventProvider"; @@ -86,47 +92,42 @@ public Metadata getMetadata() { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, - EvaluationContext ctx) { - // TODO Auto-generated method stub + EvaluationContext ctx) { throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'"); } @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, - EvaluationContext ctx) { - // TODO Auto-generated method stub + EvaluationContext ctx) { throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'"); } @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, - EvaluationContext ctx) { - // TODO Auto-generated method stub + EvaluationContext ctx) { throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'"); } @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, - EvaluationContext ctx) { - // TODO Auto-generated method stub + EvaluationContext ctx) { throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'"); } @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, - EvaluationContext ctx) { - // TODO Auto-generated method stub + EvaluationContext ctx) { throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'"); } @Override - public ProviderState getState() { - return ProviderState.READY; + public void attach(TriConsumer onEmit) { + super.attach(onEmit); } } @SuppressWarnings("unchecked") private TriConsumer mockOnEmit() { - return (TriConsumer)mock(TriConsumer.class); + return (TriConsumer) mock(TriConsumer.class); } } \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java index 6b70617f6..41bcf86c4 100644 --- a/src/test/java/dev/openfeature/sdk/EventsTest.java +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -1,26 +1,22 @@ package dev.openfeature.sdk; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -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 java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; - +import dev.openfeature.sdk.testutils.TestEventsProvider; +import io.cucumber.java.AfterAll; +import lombok.SneakyThrows; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; -import dev.openfeature.sdk.testutils.TestEventsProvider; -import io.cucumber.java.AfterAll; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; class EventsTest { @@ -29,7 +25,7 @@ class EventsTest { @AfterAll public static void resetDefaultProvider() { - OpenFeatureAPI.getInstance().setProvider(new NoOpProvider()); + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); } @Nested @@ -53,7 +49,7 @@ void apiInitReady() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); OpenFeatureAPI.getInstance().onProviderReady(handler); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(any()); } @@ -90,7 +86,7 @@ void apiShouldPropagateEvents() { final String name = "apiShouldPropagateEvents"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); @@ -113,7 +109,7 @@ void apiShouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); OpenFeatureAPI.getInstance().onProviderReady(handler1); OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler2); @@ -153,7 +149,7 @@ void shouldPropagateDefaultAndAnon() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProvider(provider); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); Client client = OpenFeatureAPI.getInstance().getClient(); client.onProviderStale(handler); @@ -170,7 +166,7 @@ void shouldPropagateDefaultAndNamed() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProvider(provider); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderStale(handler); @@ -199,7 +195,7 @@ void initReadyProviderBefore() { Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderReady(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(argThat(details -> details.getDomain().equals(name))); } @@ -213,7 +209,7 @@ void initReadyProviderAfter() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderReady(handler); verify(handler, timeout(TIMEOUT).atLeastOnce()) @@ -272,7 +268,7 @@ void shouldPropagateBefore() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderConfigurationChanged(handler); @@ -292,7 +288,7 @@ void shouldPropagateAfter() { Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderConfigurationChanged(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build()); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getDomain().equals(name))); @@ -314,7 +310,7 @@ void shouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderReady(handler1); @@ -337,21 +333,21 @@ void shouldSupportAllEventTypes() { @Test @DisplayName("shutdown provider should not run handlers") - void shouldNotRunHandlers() { + void shouldNotRunHandlers() { final Consumer handler1 = mockHandler(); final Consumer handler2 = mockHandler(); final String name = "shouldNotRunHandlers"; TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider1); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); Client client = OpenFeatureAPI.getInstance().getClient(name); // attached handlers OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); client.onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().setProvider(name, provider2); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); // wait for the new provider to be ready and make sure things are cleaned up. await().until(() -> provider1.isShutDown()); @@ -377,8 +373,8 @@ void otherClientHandlersShouldNotRun() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name1, provider1); - OpenFeatureAPI.getInstance().setProvider(name2, provider2); + OpenFeatureAPI.getInstance().setProviderAndWait(name1, provider1); + OpenFeatureAPI.getInstance().setProviderAndWait(name2, provider2); Client client1 = OpenFeatureAPI.getInstance().getClient(name1); Client client2 = OpenFeatureAPI.getInstance().getClient(name2); @@ -402,11 +398,11 @@ void boundShouldNotRunWithDefault() { TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(defaultProvider); + OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderConfigurationChanged(handlerNotToRun); - OpenFeatureAPI.getInstance().setProvider(name, namedProvider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, namedProvider); // await the new provider to make sure the old one is shut down await().until(() -> namedProvider.getState().equals(ProviderState.READY)); @@ -415,7 +411,7 @@ void boundShouldNotRunWithDefault() { defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(handlerNotToRun, after(TIMEOUT).never()).accept(any()); - OpenFeatureAPI.getInstance().setProvider(new NoOpProvider()); + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); } @Test @@ -427,7 +423,7 @@ void unboundShouldRunWithDefault() { final Consumer handlerToRun = mockHandler(); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(defaultProvider); + OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderConfigurationChanged(handlerToRun); @@ -439,7 +435,7 @@ void unboundShouldRunWithDefault() { defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); verify(handlerToRun, timeout(TIMEOUT)).accept(any()); - OpenFeatureAPI.getInstance().setProvider(new NoOpProvider()); + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); } @Test @@ -453,7 +449,7 @@ void handlersRunIfOneThrows() { final Consumer lastHandler = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client1 = OpenFeatureAPI.getInstance().getClient(name); @@ -477,7 +473,7 @@ void shouldHaveAllProperties() { final String name = "shouldHaveAllProperties"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client = OpenFeatureAPI.getInstance().getClient(name); // attached handlers @@ -520,8 +516,8 @@ void matchingReadyEventsMustRunImmediately() { final Consumer handler = mockHandler(); // provider which is already ready - TestEventsProvider provider = new TestEventsProvider(ProviderState.READY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + TestEventsProvider provider = new TestEventsProvider(); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); // should run even thought handler was added after ready Client client = OpenFeatureAPI.getInstance().getClient(name); @@ -535,13 +531,16 @@ void matchingReadyEventsMustRunImmediately() { void matchingStaleEventsMustRunImmediately() { final String name = "matchingEventsMustRunImmediately"; final Consumer handler = mockHandler(); + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already stale - TestEventsProvider provider = new TestEventsProvider(ProviderState.STALE); - OpenFeatureAPI.getInstance().setProvider(name, provider); + TestEventsProvider provider = TestEventsProvider.newInitializedTestEventsProvider(); + Client client = api.getClient(name); + api.setProviderAndWait(name, provider); + provider.emitProviderStale(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.STALE); // should run even thought handler was added after stale - Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderStale(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -552,13 +551,17 @@ void matchingStaleEventsMustRunImmediately() { void matchingErrorEventsMustRunImmediately() { final String name = "matchingEventsMustRunImmediately"; final Consumer handler = mockHandler(); + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already in error - TestEventsProvider provider = new TestEventsProvider(ProviderState.ERROR); - OpenFeatureAPI.getInstance().setProvider(name, provider); + TestEventsProvider provider = new TestEventsProvider(); + Client client = api.getClient(name); + api.setProviderAndWait(name, provider); + provider.emitProviderError(ProviderEventDetails.builder().build()); + assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR); + // should run even thought handler was added after error - Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderError(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -573,7 +576,7 @@ void mustPersistAcrossChanges() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider1); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); Client client = OpenFeatureAPI.getInstance().getClient(name); client.onProviderConfigurationChanged(handler); @@ -583,8 +586,7 @@ void mustPersistAcrossChanges() { verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches)); // wait for the new provider to be ready. - OpenFeatureAPI.getInstance().setProvider(name, provider2); - await().until(() -> provider2.getState().equals(ProviderState.READY)); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); // verify that with the new provider under the same name, the handler is called // again. @@ -594,16 +596,17 @@ void mustPersistAcrossChanges() { @Nested class HandlerRemoval { - @Specification(number="5.2.7", text="The API and client MUST provide a function allowing the removal of event handlers.") + @Specification(number = "5.2.7", text = "The API and client MUST provide a function allowing the removal of event handlers.") @Test @DisplayName("should not run removed events") + @SneakyThrows void removedEventsShouldNotRun() { final String name = "removedEventsShouldNotRun"; final Consumer handler1 = mockHandler(); final Consumer handler2 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProvider(name, provider); + OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); Client client = OpenFeatureAPI.getInstance().getClient(name); // attached handlers @@ -615,7 +618,7 @@ void removedEventsShouldNotRun() { // emit event provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build()); - + // both global and client handlers should not run. verify(handler1, after(TIMEOUT).never()).accept(any()); verify(handler2, never()).accept(any()); diff --git a/src/test/java/dev/openfeature/sdk/FeatureProviderStateManagerTest.java b/src/test/java/dev/openfeature/sdk/FeatureProviderStateManagerTest.java new file mode 100644 index 000000000..9d05524f1 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/FeatureProviderStateManagerTest.java @@ -0,0 +1,181 @@ +package dev.openfeature.sdk; + +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.GeneralError; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FeatureProviderStateManagerTest { + + private FeatureProviderStateManager wrapper; + private TestDelegate testDelegate; + + @BeforeEach + public void setUp() { + testDelegate = new TestDelegate(); + wrapper = new FeatureProviderStateManager(testDelegate); + } + + @SneakyThrows + @Test + void shouldOnlyCallInitOnce() { + wrapper.initialize(null); + wrapper.initialize(null); + assertThat(testDelegate.initCalled.get()).isOne(); + } + + @SneakyThrows + @Test + void shouldCallInitTwiceWhenShutDownInTheMeantime() { + wrapper.initialize(null); + wrapper.shutdown(); + wrapper.initialize(null); + assertThat(testDelegate.initCalled.get()).isEqualTo(2); + assertThat(testDelegate.shutdownCalled.get()).isOne(); + } + + @Test + void shouldSetStateToNotReadyAfterConstruction() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + } + + @SneakyThrows + @Test + @Specification(number = "1.7.3", text = "The client's provider status accessor MUST indicate READY if the initialize function of the associated provider terminates normally.") + void shouldSetStateToReadyAfterInit() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.initialize(null); + assertThat(wrapper.getState()).isEqualTo(ProviderState.READY); + } + + @SneakyThrows + @Test + void shouldSetStateToNotReadyAfterShutdown() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.initialize(null); + assertThat(wrapper.getState()).isEqualTo(ProviderState.READY); + wrapper.shutdown(); + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + } + + @Specification(number = "1.7.4", text = "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.") + @Test + void shouldSetStateToErrorAfterErrorOnInit() { + testDelegate.throwOnInit = new Exception(); + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + assertThrows(Exception.class, () -> wrapper.initialize(null)); + assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR); + } + + @Specification(number = "1.7.4", text = "The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider terminates abnormally.") + @Test + void shouldSetStateToErrorAfterOpenFeatureErrorOnInit() { + testDelegate.throwOnInit = new GeneralError(); + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + assertThrows(GeneralError.class, () -> wrapper.initialize(null)); + assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR); + } + + @Specification(number = "1.7.5", text = "The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider terminates abnormally and indicates error code PROVIDER_FATAL.") + @Test + void shouldSetStateToErrorAfterFatalErrorOnInit() { + testDelegate.throwOnInit = new FatalError(); + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + assertThrows(FatalError.class, () -> wrapper.initialize(null)); + assertThat(wrapper.getState()).isEqualTo(ProviderState.FATAL); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldSetTheStateToReadyWhenAReadyEventIsEmitted() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.onEmit(ProviderEvent.PROVIDER_READY, null); + assertThat(wrapper.getState()).isEqualTo(ProviderState.READY); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldSetTheStateToStaleWhenAStaleEventIsEmitted() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.onEmit(ProviderEvent.PROVIDER_STALE, null); + assertThat(wrapper.getState()).isEqualTo(ProviderState.STALE); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldSetTheStateToErrorWhenAnErrorEventIsEmitted() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.onEmit( + ProviderEvent.PROVIDER_ERROR, + ProviderEventDetails.builder().errorCode(ErrorCode.GENERAL).build() + ); + assertThat(wrapper.getState()).isEqualTo(ProviderState.ERROR); + } + + @Specification(number = "5.3.5", text = "If the provider emits an event, the value of the client's provider status MUST be updated accordingly.") + @Test + void shouldSetTheStateToFatalWhenAFatalErrorEventIsEmitted() { + assertThat(wrapper.getState()).isEqualTo(ProviderState.NOT_READY); + wrapper.onEmit( + ProviderEvent.PROVIDER_ERROR, + ProviderEventDetails.builder().errorCode(ErrorCode.PROVIDER_FATAL).build() + ); + assertThat(wrapper.getState()).isEqualTo(ProviderState.FATAL); + } + + static class TestDelegate extends EventProvider { + private final AtomicInteger initCalled = new AtomicInteger(); + private final AtomicInteger shutdownCalled = new AtomicInteger(); + private @Nullable Exception throwOnInit; + + @Override + public Metadata getMetadata() { + return null; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return null; + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return null; + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return null; + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return null; + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return null; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + initCalled.incrementAndGet(); + if (throwOnInit != null) { + throw throwOnInit; + } + } + + @Override + public void shutdown() { + shutdownCalled.incrementAndGet(); + } + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index b4978cb4b..a2316a59c 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -1,28 +1,11 @@ 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.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.assertSame; -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.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 java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.awaitility.Awaitility; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.fixtures.HookFixtures; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import dev.openfeature.sdk.testutils.TestEventsProvider; +import lombok.SneakyThrows; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,12 +13,17 @@ import org.simplify4u.slf4jmock.LoggerMock; import org.slf4j.Logger; -import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.providers.memory.InMemoryProvider; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; -import dev.openfeature.sdk.testutils.TestEventsProvider; -import lombok.SneakyThrows; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; class FlagEvaluationSpecTest implements HookFixtures { @@ -47,6 +35,14 @@ private Client _client() { return api.getClient(); } + @SneakyThrows + private Client _initializedClient() { + TestEventsProvider provider = new TestEventsProvider(); + provider.initialize(null); + FeatureProviderTestUtils.setFeatureProvider(provider); + return api.getClient(); + } + @BeforeEach void getApiInstance() { api = OpenFeatureAPI.getInstance(); @@ -82,12 +78,14 @@ void getApiInstance() { @Test void providerAndWait() { FeatureProvider provider = new TestEventsProvider(500); OpenFeatureAPI.getInstance().setProviderAndWait(provider); - assertThat(api.getProvider().getState()).isEqualTo(ProviderState.READY); + Client client = api.getClient(); + assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); provider = new TestEventsProvider(500); String providerName = "providerAndWait"; OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider); - assertThat(api.getProvider(providerName).getState()).isEqualTo(ProviderState.READY); + Client client2 = api.getClient(providerName); + assertThat(client2.getProviderState()).isEqualTo(ProviderState.READY); } @SneakyThrows @@ -103,17 +101,13 @@ void getApiInstance() { @Specification(number="2.4.5", text="The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.") @Test void shouldReturnNotReadyIfNotInitialized() { - FeatureProvider provider = new InMemoryProvider(new HashMap<>()) { - @Override - public void initialize(EvaluationContext evaluationContext) throws Exception { - Awaitility.await().wait(3000); - } - }; + FeatureProvider provider = new TestEventsProvider(100); String providerName = "shouldReturnNotReadyIfNotInitialized"; OpenFeatureAPI.getInstance().setProvider(providerName, provider); - assertThat(api.getProvider(providerName).getState()).isEqualTo(ProviderState.NOT_READY); Client client = OpenFeatureAPI.getInstance().getClient(providerName); - assertEquals(ErrorCode.PROVIDER_NOT_READY, client.getBooleanDetails("return_error_when_not_initialized", false).getErrorCode()); + FlagEvaluationDetails details = client.getBooleanDetails("return_error_when_not_initialized", false); + assertEquals(ErrorCode.PROVIDER_NOT_READY, details.getErrorCode()); + assertEquals(Reason.ERROR.toString(), details.getReason()); } @Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.") @@ -240,8 +234,9 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } @Specification(number="1.5.1", text="The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.") - @Test void hooks() { - Client c = _client(); + @SneakyThrows + @Test void hooks() { + Client c = _initializedClient(); Hook clientHook = mockBooleanHook(); Hook invocationHook = mockBooleanHook(); c.addHooks(clientHook); @@ -259,10 +254,29 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Test void broken_provider() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); - assertFalse(c.getBooleanValue("key", false)); - FlagEvaluationDetails details = c.getBooleanDetails("key", false); + boolean defaultValue = false; + assertFalse(c.getBooleanValue("key", defaultValue)); + FlagEvaluationDetails details = c.getBooleanDetails("key", defaultValue); + assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode()); + assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage()); + assertEquals(Reason.ERROR.toString(), details.getReason()); + assertEquals(defaultValue, details.getValue()); + } + + @Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.") + @Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.") + @Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.") + @Specification(number="1.4.13", 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()); + Client c = api.getClient(); + boolean defaultValue = false; + assertFalse(c.getBooleanValue("key", defaultValue)); + FlagEvaluationDetails details = c.getBooleanDetails("key", defaultValue); assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode()); assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage()); + assertEquals(Reason.ERROR.toString(), details.getReason()); + assertEquals(defaultValue, details.getValue()); } @Specification(number="1.4.11", text="Methods, functions, or operations on the client SHOULD NOT write log messages.") diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index 2554457ab..4609c8d51 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -3,34 +3,20 @@ 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 lombok.SneakyThrows; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; -import java.util.ArrayList; -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 java.util.*; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.fail; -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.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class HookSpecTest implements HookFixtures { @AfterEach @@ -39,8 +25,9 @@ void emptyApiHooks() { OpenFeatureAPI.getInstance().clearHooks(); } - @Specification(number="4.1.3", text="The flag key, flag type, and default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.") - @Test void immutableValues() { + @Specification(number = "4.1.3", text = "The flag key, flag type, and default value properties MUST be immutable. If the language does not support immutability, the hook MUST NOT modify these properties.") + @Test + void immutableValues() { try { HookContext.class.getMethod("setFlagKey"); fail("Shouldn't be able to find this method"); @@ -63,8 +50,9 @@ void emptyApiHooks() { } } - @Specification(number="4.1.1", text="Hook context MUST provide: the flag key, flag value type, evaluation context, and the default value.") - @Test void nullish_properties_on_hookcontext() { + @Specification(number = "4.1.1", text = "Hook context MUST provide: the flag key, flag value type, evaluation context, and the default value.") + @Test + void nullish_properties_on_hookcontext() { // missing ctx try { HookContext.builder() @@ -127,8 +115,9 @@ void emptyApiHooks() { } - @Specification(number="4.1.2", text="The hook context SHOULD provide: access to the client metadata and the provider metadata fields.") - @Test void optional_properties() { + @Specification(number = "4.1.2", text = "The hook context SHOULD provide: access to the client metadata and the provider metadata fields.") + @Test + void optional_properties() { // don't specify HookContext.builder() .flagKey("key") @@ -156,10 +145,11 @@ void emptyApiHooks() { .build(); } - @Specification(number="4.3.2.1", text="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() { + @Specification(number = "4.3.2.1", text = "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.setProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenProvider()); Client client = api.getClient(); Hook evalHook = mockBooleanHook(); @@ -169,13 +159,15 @@ void emptyApiHooks() { verify(evalHook, times(1)).before(any(), any()); } - @Test void feo_has_hook_list() { + @Test + void feo_has_hook_list() { FlagEvaluationOptions feo = FlagEvaluationOptions.builder() .build(); assertNotNull(feo.getHooks()); } - @Test void error_hook_run_during_non_finally_stage() { + @Test + void error_hook_run_during_non_finally_stage() { final boolean[] error_called = {false}; Hook h = mockBooleanHook(); doThrow(RuntimeException.class).when(h).finallyAfter(any(), any()); @@ -184,7 +176,8 @@ void emptyApiHooks() { } - @Test void error_hook_must_run_if_resolution_details_returns_an_error_code() { + @Test + void error_hook_must_run_if_resolution_details_returns_an_error_code() { String errorMessage = "not found..."; @@ -197,8 +190,8 @@ void emptyApiHooks() { .build()); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(provider); - Client client = api.getClient(); + FeatureProviderTestUtils.setFeatureProvider("errorHookMustRun", provider); + Client client = api.getClient("errorHookMustRun"); client.getBooleanValue("key", false, invocationCtx, FlagEvaluationOptions.builder() .hook(hook) @@ -209,7 +202,7 @@ void emptyApiHooks() { verify(hook, times(1)).before(any(), any()); verify(hook, times(1)).error(any(), captor.capture(), any()); verify(hook, times(1)).finallyAfter(any(), any()); - verify(hook, never()).after(any(),any(), any()); + verify(hook, never()).after(any(), any(), any()); Exception exception = captor.getValue(); assertEquals(errorMessage, exception.getMessage()); @@ -217,15 +210,16 @@ void emptyApiHooks() { } - @Specification(number="4.3.6", text="The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.") - @Specification(number="4.3.7", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.") - @Specification(number="4.3.8", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.") - @Specification(number="4.4.1", text="The API, Client, Provider, and invocation MUST have a method for registering hooks.") - @Specification(number="4.4.2", text="Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API") - @Test void hook_eval_order() { + @Specification(number = "4.3.6", text = "The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.") + @Specification(number = "4.3.7", text = "The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.") + @Specification(number = "4.3.8", text = "The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.") + @Specification(number = "4.4.1", text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.") + @Specification(number = "4.4.2", text = "Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API") + @Test + void hook_eval_order() { List evalOrder = new ArrayList<>(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new NoOpProvider() { + api.setProviderAndWait("evalOrder", new TestEventsProvider() { public List getProviderHooks() { return Collections.singletonList(new BooleanHook() { @@ -277,7 +271,7 @@ public void finallyAfter(HookContext ctx, Map hints) { } }); - Client c = api.getClient(); + Client c = api.getClient("evalOrder"); c.addHooks(new BooleanHook() { @Override public Optional before(HookContext ctx, Map hints) { @@ -302,64 +296,67 @@ public void finallyAfter(HookContext ctx, Map hints) { }); c.getBooleanValue("key", false, null, FlagEvaluationOptions - .builder() - .hook(new BooleanHook() { - @Override - public Optional before(HookContext ctx, Map hints) { - evalOrder.add("invocation before"); - return null; - } - - @Override - public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { - evalOrder.add("invocation after"); - } - - @Override - public void error(HookContext ctx, Exception error, Map hints) { - evalOrder.add("invocation error"); - } - - @Override - public void finallyAfter(HookContext ctx, Map hints) { - evalOrder.add("invocation finally"); - } - }) - .build()); + .builder() + .hook(new BooleanHook() { + @Override + public Optional before(HookContext ctx, Map hints) { + evalOrder.add("invocation before"); + return null; + } + + @Override + public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { + evalOrder.add("invocation after"); + } + + @Override + public void error(HookContext ctx, Exception error, Map hints) { + evalOrder.add("invocation error"); + } + + @Override + public void finallyAfter(HookContext ctx, Map hints) { + evalOrder.add("invocation finally"); + } + }) + .build()); List expectedOrder = Arrays.asList( - "api before", "client before", "invocation before", "provider before", - "provider after", "invocation after", "client after", "api after", - "provider error", "invocation error", "client error", "api error", - "provider finally", "invocation finally", "client finally", "api finally"); + "api before", "client before", "invocation before", "provider before", + "provider after", "invocation after", "client after", "api after", + "provider error", "invocation error", "client error", "api error", + "provider finally", "invocation finally", "client finally", "api finally"); assertEquals(expectedOrder, evalOrder); } - @Specification(number="4.4.6", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.") - @Test void error_stops_before() { + @Specification(number = "4.4.6", text = "If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.") + @Test + void error_stops_before() { Hook h = mockBooleanHook(); doThrow(RuntimeException.class).when(h).before(any(), any()); Hook h2 = mockBooleanHook(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenProvider()); Client c = api.getClient(); c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder() .hook(h2) .hook(h) .build()); - verify(h, times(1)).before(any(), any()); - verify(h2, times(0)).before(any(), any()); + verify(h, times(1)).before(any(), any()); + verify(h2, times(0)).before(any(), any()); } - @Specification(number="4.4.6", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.") - @Test void error_stops_after() { + @Specification(number = "4.4.6", text = "If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.") + @SneakyThrows + @Test + void error_stops_after() { Hook h = mockBooleanHook(); doThrow(RuntimeException.class).when(h).after(any(), any(), any()); Hook h2 = mockBooleanHook(); - Client c = getClient(null); + Client c = getClient(TestEventsProvider.newInitializedTestEventsProvider()); c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder() .hook(h) @@ -369,11 +366,13 @@ public void finallyAfter(HookContext ctx, Map hints) { verify(h2, times(0)).after(any(), any(), any()); } - @Specification(number="4.2.1", text="hook hints MUST be a structure supports definition of arbitrary properties, with keys of type string, and values of type boolean | string | number | datetime | structure..") - @Specification(number="4.5.2", text="hook hints MUST be passed to each hook.") - @Specification(number="4.2.2.1", text="Condition: Hook hints MUST be immutable.") - @Specification(number="4.5.3", text="The hook MUST NOT alter the hook hints structure.") - @Test void hook_hints() { + @Specification(number = "4.2.1", text = "hook hints MUST be a structure supports definition of arbitrary properties, with keys of type string, and values of type boolean | string | number | datetime | structure..") + @Specification(number = "4.5.2", text = "hook hints MUST be passed to each hook.") + @Specification(number = "4.2.2.1", text = "Condition: Hook hints MUST be immutable.") + @Specification(number = "4.5.3", text = "The hook MUST NOT alter the hook hints structure.") + @SneakyThrows + @Test + void hook_hints() { String hintKey = "My hint key"; Client client = getClient(null); Hook mutatingHook = new BooleanHook() { @@ -409,14 +408,16 @@ public void finallyAfter(HookContext ctx, Map hints) { .build()); } - @Specification(number="4.5.1", text="Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.") - @Test void missing_hook_hints() { + @Specification(number = "4.5.1", text = "Flag evaluation options MAY contain hook hints, a map of data to be provided to hook invocations.") + @Test + void missing_hook_hints() { FlagEvaluationOptions feo = FlagEvaluationOptions.builder().build(); assertNotNull(feo.getHookHints()); assertTrue(feo.getHookHints().isEmpty()); } - @Test void flag_eval_hook_order() { + @Test + void flag_eval_hook_order() { Hook hook = mockBooleanHook(); FeatureProvider provider = mock(FeatureProvider.class); when(provider.getBooleanEvaluation(any(), any(), any())) @@ -437,12 +438,13 @@ public void finallyAfter(HookContext ctx, Map hints) { order.verify(hook).finallyAfter(any(), any()); } - @Specification(number="4.4.5", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.") - @Specification(number="4.4.7", text="If an error occurs in the before hooks, the default value MUST be returned.") - @Test void error_hooks__before() { + @Specification(number = "4.4.5", text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.") + @Specification(number = "4.4.7", text = "If an error occurs in the before hooks, the default value MUST be returned.") + @Test + void error_hooks__before() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).before(any(), any()); - Client client = getClient(null); + Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); Boolean value = client.getBooleanValue("key", false, new ImmutableContext(), FlagEvaluationOptions.builder().hook(hook).build()); verify(hook, times(1)).before(any(), any()); @@ -450,18 +452,20 @@ public void finallyAfter(HookContext ctx, Map hints) { assertEquals(false, value, "Falls through to the default."); } - @Specification(number="4.4.5", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.") - @Test void error_hooks__after() { + @Specification(number = "4.4.5", text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked.") + @Test + void error_hooks__after() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).after(any(), any(), any()); - Client client = getClient(null); + Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider()); client.getBooleanValue("key", false, new ImmutableContext(), FlagEvaluationOptions.builder().hook(hook).build()); verify(hook, times(1)).after(any(), any(), any()); verify(hook, times(1)).error(any(), any(), any()); } - @Test void multi_hooks_early_out__before() { + @Test + void multi_hooks_early_out__before() { Hook hook = mockBooleanHook(); Hook hook2 = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).before(any(), any()); @@ -483,7 +487,8 @@ public void finallyAfter(HookContext ctx, Map hints) { @Specification(number = "4.1.4", text = "The evaluation context MUST be mutable only within the before hook.") @Specification(number = "4.3.4", text = "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).") - @Test void beforeContextUpdated() { + @Test + void beforeContextUpdated() { String targetingKey = "test-key"; EvaluationContext ctx = new ImmutableContext(targetingKey); Hook hook = mockBooleanHook(); @@ -508,15 +513,16 @@ public void finallyAfter(HookContext ctx, Map hints) { } - @Specification(number="4.3.5", text="When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.") - @Test void mergeHappensCorrectly() { - Map attributes= new HashMap<>(); + @Specification(number = "4.3.5", text = "When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.") + @Test + void mergeHappensCorrectly() { + Map attributes = new HashMap<>(); attributes.put("test", new Value("works")); attributes.put("another", new Value("exists")); EvaluationContext hookCtx = new ImmutableContext(attributes); - Map attributes1= new HashMap<>(); + Map attributes1 = new HashMap<>(); attributes1.put("something", new Value("here")); attributes1.put("test", new Value("broken")); EvaluationContext invocationCtx = new ImmutableContext(attributes1); @@ -545,8 +551,9 @@ public void finallyAfter(HookContext ctx, Map hints) { assertEquals("here", ec.getValue("something").asString()); } - @Specification(number="4.4.3", text="If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.") - @Test void first_finally_broken() { + @Specification(number = "4.4.3", text = "If a finally hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining finally hooks.") + @Test + void first_finally_broken() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).before(any(), any()); doThrow(RuntimeException.class).when(hook).finallyAfter(any(), any()); @@ -565,8 +572,9 @@ public void finallyAfter(HookContext ctx, Map hints) { order.verify(hook).finallyAfter(any(), any()); } - @Specification(number="4.4.4", text="If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.") - @Test void first_error_broken() { + @Specification(number = "4.4.4", text = "If an error hook abnormally terminates, evaluation MUST proceed, including the execution of any remaining error hooks.") + @Test + void first_error_broken() { Hook hook = mockBooleanHook(); doThrow(RuntimeException.class).when(hook).before(any(), any()); doThrow(RuntimeException.class).when(hook).error(any(), any(), any()); @@ -588,25 +596,28 @@ public void finallyAfter(HookContext ctx, Map hints) { private Client getClient(FeatureProvider provider) { OpenFeatureAPI api = OpenFeatureAPI.getInstance(); if (provider == null) { - FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); + FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); } else { FeatureProviderTestUtils.setFeatureProvider(provider); } return api.getClient(); } - @Specification(number="4.3.1", text="Hooks MUST specify at least one stage.") - @Test void default_methods_so_impossible() {} + @Specification(number = "4.3.1", text = "Hooks MUST specify at least one stage.") + @Test + void default_methods_so_impossible() { + } - @Specification(number="4.3.9.1", text="Instead of finally, finallyAfter SHOULD be used.") + @Specification(number = "4.3.9.1", text = "Instead of finally, finallyAfter SHOULD be used.") @SneakyThrows - @Test void doesnt_use_finally() { + @Test + void doesnt_use_finally() { assertThatCode(() -> Hook.class.getMethod("finally", HookContext.class, Map.class)) - .as("Not possible. Finally is a reserved word.") - .isInstanceOf(NoSuchMethodException.class); + .as("Not possible. Finally is a reserved word.") + .isInstanceOf(NoSuchMethodException.class); assertThatCode(() -> Hook.class.getMethod("finallyAfter", HookContext.class, Map.class)) - .doesNotThrowAnyException(); + .doesNotThrowAnyException(); } } diff --git a/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java b/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java new file mode 100644 index 000000000..a169107f1 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java @@ -0,0 +1,51 @@ +package dev.openfeature.sdk; + +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class MutableTrackingEventDetailsTest { + + + @Test + void hasDefaultValue() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(); + assertFalse(track.getValue().isPresent()); + } + + @Test + void shouldUseCorrectValue() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(3); + assertThat(track.getValue()).hasValue(3); + } + + @Test + void shouldStoreAttributes() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(); + track.add("key0", true); + track.add("key1", 1); + track.add("key2", "2"); + track.add("key3", 1d); + track.add("key4", 4); + track.add("key5", Instant.parse("2023-12-03T10:15:30Z")); + track.add("key6", new MutableContext()); + track.add("key7", new Value(7)); + track.add("key8", Lists.newArrayList(new Value(8), new Value(9))); + + assertEquals(new Value(true), track.getValue("key0")); + assertEquals(new Value(1), track.getValue("key1")); + assertEquals(new Value("2"), track.getValue("key2")); + assertEquals(new Value(1d), track.getValue("key3")); + assertEquals(new Value(4), track.getValue("key4")); + assertEquals(new Value(Instant.parse("2023-12-03T10:15:30Z")), track.getValue("key5")); + assertEquals(new Value(new MutableContext()), track.getValue("key6")); + assertEquals(new Value(7), track.getValue("key7")); + assertArrayEquals(new Object[]{new Value(8), new Value(9)}, track.getValue("key8").asList().toArray()); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index 74298f72f..026170a78 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -1,17 +1,20 @@ package dev.openfeature.sdk; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.junit.jupiter.api.Assertions.assertEquals; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import dev.openfeature.sdk.testutils.TestEventsProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashMap; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import dev.openfeature.sdk.providers.memory.InMemoryProvider; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +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.verify; class OpenFeatureAPITest { @@ -33,7 +36,7 @@ void namedProviderTest() { .isEqualTo(api.getProviderMetadata("namedProviderTest").getName()); } - @Specification(number="1.1.3", text="The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.") + @Specification(number = "1.1.3", text = "The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.") @Test void namedProviderOverwrittenTest() { String domain = "namedProviderOverwrittenTest"; @@ -87,4 +90,35 @@ void setEvaluationContextShouldAllowChaining() { OpenFeatureClient result = client.setEvaluationContext(ctx); assertEquals(client, result); } + + @Test + void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception { + String domain = "namedProviderOverwrittenTest"; + FeatureProvider provider1 = new NoOpProvider(); + FeatureProvider provider2 = new TestEventsProvider(); + FeatureProviderTestUtils.setFeatureProvider(domain, provider1); + FeatureProviderTestUtils.setFeatureProvider(domain, provider2); + + provider2.initialize(null); + + assertThat(OpenFeatureAPI.getInstance().getClient(domain).getProviderState()) + .isEqualTo(ProviderState.READY); + } + + + @Test + void featureProviderTrackIsCalled() throws Exception { + FeatureProvider featureProvider = mock(FeatureProvider.class); + FeatureProviderTestUtils.setFeatureProvider(featureProvider); + + OpenFeatureAPI.getInstance() + .getClient() + .track("track-event", + new ImmutableContext(), + new MutableTrackingEventDetails(22.2f)); + + verify(featureProvider).initialize(any()); + verify(featureProvider).getMetadata(); + verify(featureProvider).track(any(), any(), any()); + } } diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index d6340a844..3c82fd656 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -1,43 +1,49 @@ package dev.openfeature.sdk; -import java.util.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; -import dev.openfeature.sdk.fixtures.HookFixtures; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.*; +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.mockito.Mockito; import org.simplify4u.slf4jmock.LoggerMock; import org.slf4j.Logger; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.fixtures.HookFixtures; +import dev.openfeature.sdk.testutils.TestEventsProvider; class OpenFeatureClientTest implements HookFixtures { private Logger logger; - @BeforeEach void set_logger() { + @BeforeEach + void set_logger() { logger = Mockito.mock(Logger.class); LoggerMock.setMock(OpenFeatureClient.class, logger); } - @AfterEach void reset_logs() { + @AfterEach + void reset_logs() { LoggerMock.setMock(OpenFeatureClient.class, logger); } + @Test @DisplayName("should not throw exception if hook has different type argument than hookContext") void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() { - OpenFeatureAPI api = mock(OpenFeatureAPI.class); - when(api.getProvider(any())).thenReturn(new DoSomethingProvider()); - when(api.getHooks()).thenReturn(Arrays.asList(mockBooleanHook(), mockStringHook())); - - OpenFeatureClient client = new OpenFeatureClient(api, "name", "version"); - + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider()); + Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext"); + client.addHooks(mockBooleanHook(), mockStringHook()); FlagEvaluationDetails actual = client.getBooleanDetails("feature key", Boolean.FALSE); assertThat(actual.getValue()).isTrue(); @@ -49,31 +55,6 @@ void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() { Mockito.verify(logger, never()).error(anyString(), any(), any()); } - @Test - void mergeContextTest() { - String flag = "feature key"; - boolean defaultValue = false; - String targetingKey = "targeting key"; - EvaluationContext ctx = new ImmutableContext(targetingKey, new HashMap<>()); - OpenFeatureAPI api = mock(OpenFeatureAPI.class); - FeatureProvider mockProvider = mock(FeatureProvider.class); - // this makes it so that true is returned only if the targeting key set at the client level is honored - when(mockProvider.getBooleanEvaluation( - eq(flag), eq(defaultValue), argThat( - context -> context.getTargetingKey().equals(targetingKey)))).thenReturn(ProviderEvaluation.builder() - .value(true).build()); - when(api.getProvider()).thenReturn(mockProvider); - when(api.getProvider(any())).thenReturn(mockProvider); - - - OpenFeatureClient client = new OpenFeatureClient(api, "name", "version"); - client.setEvaluationContext(ctx); - - FlagEvaluationDetails result = client.getBooleanDetails(flag, defaultValue); - - assertThat(result.getValue()).isTrue(); - } - @Test @DisplayName("addHooks should allow chaining by returning the same client instance") void addHooksShouldAllowChaining() { @@ -83,7 +64,7 @@ void addHooksShouldAllowChaining() { Hook hook2 = Mockito.mock(Hook.class); OpenFeatureClient result = client.addHooks(hook1, hook2); - assertEquals(client, result); + assertEquals(client, result); } @Test @@ -96,5 +77,82 @@ void setEvaluationContextShouldAllowChaining() { OpenFeatureClient result = client.setEvaluationContext(ctx); assertEquals(client, result); } - + + + @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(); + Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState"); + + assertThrows(FatalError.class, () -> api.setProviderAndWait("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState", provider)); + FlagEvaluationDetails details = client.getBooleanDetails("key", true); + assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_FATAL); + } + + @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(); + api.setProvider("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState", provider); + Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState"); + FlagEvaluationDetails details = client.getBooleanDetails("key", true); + + assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_NOT_READY); + } + + private static class MockProvider implements FeatureProvider { + private final AtomicBoolean evaluationCalled = new AtomicBoolean(); + private final ProviderState providerState; + + public MockProvider(ProviderState providerState) { + this.providerState = providerState; + } + + public boolean isEvaluationCalled() { + return evaluationCalled.get(); + } + + @Override + public ProviderState getState() { + return providerState; + } + + @Override + public Metadata getMetadata() { + return null; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + evaluationCalled.set(true); + return null; + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + evaluationCalled.set(true); + return null; + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + evaluationCalled.set(true); + return null; + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + evaluationCalled.set(true); + return null; + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + evaluationCalled.set(true); + return null; + } + } } diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java index c827c91fb..26a04d533 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java @@ -1,19 +1,11 @@ package dev.openfeature.sdk; -import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedErrorProvider; -import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedProvider; -import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedReadyProvider; -import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -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.exceptions.OpenFeatureError; +import dev.openfeature.sdk.testutils.exception.TestException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.concurrent.ExecutorService; @@ -23,13 +15,14 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import dev.openfeature.sdk.exceptions.OpenFeatureError; -import dev.openfeature.sdk.testutils.exception.TestException; +import static dev.openfeature.sdk.fixtures.ProviderFixture.*; +import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; class ProviderRepositoryTest { @@ -84,15 +77,6 @@ void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception { verify(featureProvider, timeout(TIMEOUT)).initialize(any()); } - - @Test - @DisplayName("should avoid additional initialization call if provider has been initialized already") - void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception { - FeatureProvider provider = createMockedReadyProvider(); - setFeatureProvider(provider); - - verify(provider, never()).initialize(any()); - } } @Nested @@ -132,15 +116,6 @@ void shouldImmediatelyReturnWhenCallingTheDomainProviderMutator() throws Excepti return true; }); } - - @Test - @DisplayName("should avoid additional initialization call if provider has been initialized already") - void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception { - FeatureProvider provider = createMockedReadyProvider(); - setFeatureProvider(DOMAIN_NAME, provider); - - verify(provider, never()).initialize(any()); - } } } @@ -254,11 +229,11 @@ void shouldRunLambdasOnSuccessful() { Consumer afterInit = mock(Consumer.class); Consumer afterShutdown = mock(Consumer.class); BiConsumer afterError = mock(BiConsumer.class); - + FeatureProvider oldProvider = providerRepository.getProvider(); FeatureProvider featureProvider1 = createMockedProvider(); FeatureProvider featureProvider2 = createMockedProvider(); - + setFeatureProvider(featureProvider1, afterSet, afterInit, afterShutdown, afterError); setFeatureProvider(featureProvider2); verify(afterSet, timeout(TIMEOUT)).accept(featureProvider1); @@ -275,12 +250,13 @@ void shouldRunLambdasOnError() throws Exception { Consumer afterInit = mock(Consumer.class); Consumer afterShutdown = mock(Consumer.class); BiConsumer afterError = mock(BiConsumer.class); - + FeatureProvider errorFeatureProvider = createMockedErrorProvider(); - + setFeatureProvider(errorFeatureProvider, afterSet, afterInit, afterShutdown, afterError); verify(afterSet, timeout(TIMEOUT)).accept(errorFeatureProvider); - verify(afterInit, never()).accept(any());; + verify(afterInit, never()).accept(any()); + ; verify(afterError, timeout(TIMEOUT)).accept(eq(errorFeatureProvider), any()); } } @@ -309,8 +285,8 @@ private void setFeatureProvider(FeatureProvider provider) { private void setFeatureProvider(FeatureProvider provider, Consumer afterSet, - Consumer afterInit, Consumer afterShutdown, - BiConsumer afterError) { + Consumer afterInit, Consumer afterShutdown, + BiConsumer afterError) { providerRepository.setProvider(provider, afterSet, afterInit, afterShutdown, afterError, false); waitForSettingProviderHasBeenCompleted(ProviderRepository::getProvider, provider); @@ -329,7 +305,7 @@ private void waitForSettingProviderHasBeenCompleted( .pollDelay(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(5)) .until(() -> { - return extractor.apply(providerRepository) == provider; + return extractor.apply(providerRepository).equals(provider); }); } diff --git a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java new file mode 100644 index 000000000..6d195607b --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java @@ -0,0 +1,189 @@ +package dev.openfeature.sdk; + +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 lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class TrackingSpecTest { + + private OpenFeatureAPI api; + private Client client; + + @BeforeEach + void getApiInstance() { + api = OpenFeatureAPI.getInstance(); + client = api.getClient(); + } + + + @Specification(number = "6.1.1.1", text = "The `client` MUST define a function for tracking the occurrence of " + + "a particular action or application state, with parameters `tracking event name` (string, required), " + + "`evaluation context` (optional) and `tracking event details` (optional), which returns nothing.") + @Specification(number = "6.1.2.1", text = "The `client` MUST define a function for tracking the occurrence of a " + + "particular action or application state, with parameters `tracking event name` (string, required) and " + + "`tracking event details` (optional), which returns nothing.") + @Test + @SneakyThrows + void trackMethodFulfillsSpec() { + + ImmutableContext ctx = new ImmutableContext(); + MutableTrackingEventDetails details = new MutableTrackingEventDetails(0.0f); + assertThatCode(() -> client.track("event")).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", ctx)).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", details)).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", ctx, details)).doesNotThrowAnyException(); + + assertThrows(NullPointerException.class, () -> client.track(null, ctx, details)); + assertThrows(NullPointerException.class, () -> client.track("event", null, details)); + assertThrows(NullPointerException.class, () -> client.track("event", ctx, null)); + assertThrows(NullPointerException.class, () -> client.track(null, null, null)); + assertThrows(NullPointerException.class, () -> client.track(null, ctx)); + assertThrows(NullPointerException.class, () -> client.track(null, details)); + assertThrows(NullPointerException.class, () -> client.track("event", (EvaluationContext) null)); + assertThrows(NullPointerException.class, () -> client.track("event", (TrackingEventDetails) null)); + + assertThrows(IllegalArgumentException.class, () -> client.track("")); + assertThrows(IllegalArgumentException.class, () -> client.track("", ctx)); + assertThrows(IllegalArgumentException.class, () -> client.track("", ctx, details)); + + + Class clientClass = OpenFeatureClient.class; + assertEquals( + void.class, + clientClass.getMethod("track", String.class).getReturnType(), + "The method should return void."); + assertEquals( + void.class, + clientClass.getMethod("track", String.class, EvaluationContext.class).getReturnType(), + "The method should return void."); + + assertEquals( + void.class, + clientClass.getMethod("track", String.class, EvaluationContext.class, TrackingEventDetails.class).getReturnType(), + "The method should return void."); + + + } + + @Specification(number = "6.1.3", text = "The evaluation context passed to the provider's track function " + + "MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> " + + "invocation (highest precedence), with duplicate values being overwritten.") + @Test + void contextsGetMerged() { + + api.setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + + Map apiAttr = new HashMap<>(); + apiAttr.put("my-key", new Value("hey")); + apiAttr.put("my-api-key", new Value("333")); + EvaluationContext apiCtx = new ImmutableContext(apiAttr); + api.setEvaluationContext(apiCtx); + + Map txAttr = new HashMap<>(); + txAttr.put("my-key", new Value("overwritten")); + txAttr.put("my-tx-key", new Value("444")); + EvaluationContext txCtx = new ImmutableContext(txAttr); + api.setTransactionContext(txCtx); + + Map clAttr = new HashMap<>(); + clAttr.put("my-key", new Value("overwritten-again")); + clAttr.put("my-cl-key", new Value("555")); + EvaluationContext clCtx = new ImmutableContext(clAttr); + client.setEvaluationContext(clCtx); + + FeatureProvider provider = ProviderFixture.createMockedProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + client.track("event", new MutableContext().add("my-key", "final"), new MutableTrackingEventDetails(0.0f)); + + Map expectedMap = Maps.newHashMap(); + expectedMap.put("my-key", new Value("final")); + expectedMap.put("my-api-key", new Value("333")); + expectedMap.put("my-tx-key", new Value("444")); + expectedMap.put("my-cl-key", new Value("555")); + verify(provider).track(eq("event"), argThat(ctx -> ctx.asMap().equals(expectedMap)), notNull()); + } + + @Specification(number = "6.1.4", text = "If the client's `track` function is called and the associated provider " + + "does not implement tracking, the client's `track` function MUST no-op.") + @Test + void noopProvider() { + FeatureProvider provider = spy(FeatureProvider.class); + api.setProvider(provider); + client.track("event"); + verify(provider).track(any(), any(), any()); + } + + @Specification(number = "6.2.1", text = "The `tracking event details` structure MUST define an optional numeric " + + "`value`, associating a scalar quality with an `tracking event`.") + @Specification(number = "6.2.2", text = "The `tracking event details` MUST support the inclusion of custom " + + "fields, having keys of type `string`, and values of type `boolean | string | number | structure`.") + @Test + void eventDetails() { + assertFalse(new MutableTrackingEventDetails().getValue().isPresent()); + assertFalse(new ImmutableTrackingEventDetails().getValue().isPresent()); + assertThat(new ImmutableTrackingEventDetails(2).getValue()).hasValue(2); + assertThat(new MutableTrackingEventDetails(9.87f).getValue()).hasValue(9.87f); + + + // using mutable tracking event details + Map expectedMap = Maps.newHashMap(); + expectedMap.put("my-str", new Value("str")); + expectedMap.put("my-num", new Value(1)); + expectedMap.put("my-bool", new Value(true)); + expectedMap.put("my-struct", new Value(new MutableTrackingEventDetails())); + + MutableTrackingEventDetails details = new MutableTrackingEventDetails() + .add("my-str", new Value("str")) + .add("my-num", new Value(1)) + .add("my-bool", new Value(true)) + .add("my-struct", new Value(new MutableTrackingEventDetails())); + + assertEquals(expectedMap, details.asMap()); + assertThatCode(() -> OpenFeatureAPI.getInstance().getClient(). + track( + "tracking-event-name", + new ImmutableContext(), + new MutableTrackingEventDetails())) + .doesNotThrowAnyException(); + + + // using immutable tracking event details + ImmutableMap expectedImmutable = ImmutableMap.of("my-str", new Value("str"), + "my-num", new Value(1), + "my-bool", new Value(true), + "my-struct", new Value(new ImmutableStructure()) + ); + + ImmutableTrackingEventDetails immutableDetails = new ImmutableTrackingEventDetails(2, expectedMap); + assertEquals(expectedImmutable, immutableDetails.asMap()); + assertThatCode(() -> OpenFeatureAPI.getInstance().getClient(). + track( + "tracking-event-name", + new ImmutableContext(), + new ImmutableTrackingEventDetails())) + .doesNotThrowAnyException(); + + + } + +} diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java new file mode 100644 index 000000000..cb9422e32 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -0,0 +1,74 @@ +package dev.openfeature.sdk.benchmark; + +import static dev.openfeature.sdk.testutils.TestFlagsUtils.BOOLEAN_FLAG_KEY; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.FLOAT_FLAG_KEY; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.INT_FLAG_KEY; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.OBJECT_FLAG_KEY; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.STRING_FLAG_KEY; + +import java.util.Map; +import java.util.HashMap; +import java.util.Optional; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Warmup; + +import dev.openfeature.sdk.Client; +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.NoOpProvider; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; + +/** + * Runs a large volume of flag evaluations on a VM with 1G memory and GC + * completely disabled so we can take a heap-dump. + */ +public class AllocationBenchmark { + + // 10K iterations works well with Xmx1024m (we don't want to run out of memory) + private static final int ITERATIONS = 10000; + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Fork(jvmArgsAppend = { "-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC" }) + public void run() { + + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + Map globalAttrs = new HashMap<>(); + globalAttrs.put("global", new Value(1)); + EvaluationContext globalContext = new ImmutableContext(globalAttrs); + OpenFeatureAPI.getInstance().setEvaluationContext(globalContext); + + Client client = OpenFeatureAPI.getInstance().getClient(); + + Map clientAttrs = new HashMap<>(); + clientAttrs.put("client", new Value(2)); + client.setEvaluationContext(new ImmutableContext(clientAttrs)); + client.addHooks(new Hook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(new ImmutableContext()); + } + }); + + Map invocationAttrs = new HashMap<>(); + invocationAttrs.put("invoke", new Value(3)); + EvaluationContext invocationContext = new ImmutableContext(invocationAttrs); + + for (int i = 0; i < ITERATIONS; i++) { + client.getBooleanValue(BOOLEAN_FLAG_KEY, false); + client.getStringValue(STRING_FLAG_KEY, "default"); + client.getIntegerValue(INT_FLAG_KEY, 0); + client.getDoubleValue(FLOAT_FLAG_KEY, 0.0); + client.getObjectDetails(OBJECT_FLAG_KEY, new Value(new ImmutableStructure()), invocationContext); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationProfiler.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationProfiler.java new file mode 100644 index 000000000..8051a167e --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationProfiler.java @@ -0,0 +1,124 @@ +package dev.openfeature.sdk.benchmark; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; + +import org.openjdk.jmh.infra.BenchmarkParams; +import org.openjdk.jmh.infra.IterationParams; +import org.openjdk.jmh.profile.InternalProfiler; +import org.openjdk.jmh.results.AggregationPolicy; +import org.openjdk.jmh.results.IterationResult; +import org.openjdk.jmh.results.Result; +import org.openjdk.jmh.results.ScalarResult; +import org.openjdk.jmh.util.Utils; + +/** + * Takes a heap dump (using JMAP from a separate process) after a benchmark; + * only useful if GC is disabled during the benchmark. + */ +public class AllocationProfiler implements InternalProfiler { + + public static class AllocationTotals { + long instances; + long bytes; + + public AllocationTotals(long instances, long bytes) { + this.instances = instances; + this.bytes = bytes; + } + } + + @Override + public String getDescription() { + return "Max memory heap profiler"; + } + + @Override + public void beforeIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams) { + // intentionally left blank + } + + @Override + public Collection afterIteration(BenchmarkParams benchmarkParams, IterationParams iterationParams, + IterationResult result) { + + long totalHeap = Runtime.getRuntime().totalMemory(); + AllocationTotals allocationTotals = AllocationProfiler.printHeapHistogram(System.out, 120); + + Collection results = new ArrayList<>(); + results.add(new ScalarResult("+totalHeap", totalHeap, "bytes", AggregationPolicy.MAX)); + results.add(new ScalarResult("+totalAllocatedInstances", allocationTotals.instances, "instances", + AggregationPolicy.MAX)); + results.add(new ScalarResult("+totalAllocatedBytes", allocationTotals.bytes, "bytes", AggregationPolicy.MAX)); + + return results; + } + + private static String getJmapExcutable() { + String javaHome = System.getProperty("java.home"); + String jreDir = File.separator + "jre"; + if (javaHome.endsWith(jreDir)) { + javaHome = javaHome.substring(0, javaHome.length() - jreDir.length()); + } + return (javaHome + + File.separator + + "bin" + + File.separator + + "jmap" + + (Utils.isWindows() ? ".exe" : "")); + } + + // runs JMAP executable in a new process to collect a heap dump + // heavily inspired by: https://github.com/cache2k/cache2k-benchmark/blob/master/jmh-suite/src/main/java/org/cache2k/benchmark/jmh/HeapProfiler.java + private static AllocationTotals printHeapHistogram(PrintStream out, int maxLines) { + long totalBytes = 0; + long totalInstances = 0; + boolean partial = false; + try { + Process jmapProcess = Runtime.getRuntime().exec(new String[] { + getJmapExcutable(), + "-histo:live", + Long.toString(Utils.getPid()) }); + InputStream in = jmapProcess.getInputStream(); + LineNumberReader r = new LineNumberReader(new InputStreamReader(in)); + String line; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(buffer); + while ((line = r.readLine()) != null) { + if (line.startsWith("Total")) { + printStream.println(line); + String[] tokens = line.split("\\s+"); + totalInstances += Long.parseLong(tokens[1]); + totalBytes = Long.parseLong(tokens[2]); + } else if (r.getLineNumber() <= maxLines) { + printStream.println(line); + } else { + if (!partial) { + printStream.println("truncated..."); + } + partial = true; + } + } + r.close(); + in.close(); + printStream.close(); + byte[] histogramOutput = buffer.toByteArray(); + buffer = new ByteArrayOutputStream(); + printStream = new PrintStream(buffer); + printStream.write(histogramOutput); + printStream.println(); + printStream.close(); + out.write(buffer.toByteArray()); + } catch (Exception ex) { + System.err.println("ForcedGcMemoryProfiler: error attaching / reading histogram"); + ex.printStackTrace(); + } + return new AllocationTotals(totalInstances, totalBytes); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java similarity index 62% rename from src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java rename to src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java index 2c652338d..3e0f2ee89 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java +++ b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java @@ -5,12 +5,17 @@ import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") -@SelectClasspathResource("features") +@SelectClasspathResource("features/evaluation.feature") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") -public class RunCucumberTest { - +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.evaluation") +public class EvaluationTest { + } + + + diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java similarity index 90% rename from src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java rename to src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java index 459fcefea..cf1905926 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java @@ -1,4 +1,4 @@ -package dev.openfeature.sdk.e2e; +package dev.openfeature.sdk.e2e.evaluation; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.EvaluationContext; @@ -52,7 +52,7 @@ public class StepDefinitions { @SneakyThrows @BeforeAll() - @Given("an openfeature client is registered with cache disabled") + @Given("a provider is registered") public static void setup() { Map> flags = buildFlags(); InMemoryProvider provider = new InMemoryProvider(flags); @@ -67,7 +67,7 @@ public static void setup() { // boolean value @When("a boolean flag with key {string} is evaluated with default value {string}") public void a_boolean_flag_with_key_boolean_flag_is_evaluated_with_default_value_false(String flagKey, - String defaultValue) { + String defaultValue) { this.booleanFlagValue = client.getBooleanValue(flagKey, Boolean.valueOf(defaultValue)); } @@ -117,7 +117,7 @@ public void an_object_flag_with_key_is_evaluated_with_a_null_default_value(Strin @Then("the resolved object value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively") public void the_resolved_object_value_should_be_contain_fields_and_with_values_and_respectively(String boolField, - String stringField, String numberField, String boolValue, String stringValue, int numberValue) { + String stringField, String numberField, String boolValue, String stringValue, int numberValue) { Structure structure = this.objectFlagValue.asStructure(); assertEquals(Boolean.valueOf(boolValue), structure.asMap().get(boolField).asBoolean()); @@ -132,7 +132,7 @@ public void the_resolved_object_value_should_be_contain_fields_and_with_values_a // boolean details @When("a boolean flag with key {string} is evaluated with details and default value {string}") public void a_boolean_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, - String defaultValue) { + String defaultValue) { this.booleanFlagDetails = client.getBooleanDetails(flagKey, Boolean.valueOf(defaultValue)); } @@ -148,13 +148,13 @@ public void the_resolved_boolean_value_should_be_the_variant_should_be_and_the_r // string details @When("a string flag with key {string} is evaluated with details and default value {string}") public void a_string_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, - String defaultValue) { + String defaultValue) { this.stringFlagDetails = client.getStringDetails(flagKey, defaultValue); } @Then("the resolved string details value should be {string}, the variant should be {string}, and the reason should be {string}") public void the_resolved_string_value_should_be_the_variant_should_be_and_the_reason_should_be(String expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.stringFlagDetails.getValue()); assertEquals(expectedVariant, this.stringFlagDetails.getVariant()); assertEquals(expectedReason, this.stringFlagDetails.getReason()); @@ -168,7 +168,7 @@ public void an_integer_flag_with_key_is_evaluated_with_details_and_default_value @Then("the resolved integer details value should be {int}, the variant should be {string}, and the reason should be {string}") public void the_resolved_integer_value_should_be_the_variant_should_be_and_the_reason_should_be(int expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.intFlagDetails.getValue()); assertEquals(expectedVariant, this.intFlagDetails.getVariant()); assertEquals(expectedReason, this.intFlagDetails.getReason()); @@ -182,7 +182,7 @@ public void a_float_flag_with_key_is_evaluated_with_details_and_default_value(St @Then("the resolved float details value should be {double}, the variant should be {string}, and the reason should be {string}") public void the_resolved_float_value_should_be_the_variant_should_be_and_the_reason_should_be(double expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.doubleFlagDetails.getValue()); assertEquals(expectedVariant, this.doubleFlagDetails.getVariant()); assertEquals(expectedReason, this.doubleFlagDetails.getReason()); @@ -217,7 +217,7 @@ public void the_variant_should_be_and_the_reason_should_be(String expectedVarian @When("context contains keys {string}, {string}, {string}, {string} with values {string}, {string}, {int}, {string}") public void context_contains_keys_with_values(String field1, String field2, String field3, String field4, - String value1, String value2, Integer value3, String value4) { + String value1, String value2, Integer value3, String value4) { Map attributes = new HashMap<>(); attributes.put(field1, new Value(value1)); attributes.put(field2, new Value(value2)); @@ -253,7 +253,7 @@ public void the_resolved_flag_value_is_when_the_context_is_empty(String expected // not found @When("a non-existent string flag with key {string} is evaluated with details and a default value {string}") public void a_non_existent_string_flag_with_key_is_evaluated_with_details_and_a_default_value(String flagKey, - String defaultValue) { + String defaultValue) { notFoundFlagKey = flagKey; notFoundDefaultValue = defaultValue; notFoundDetails = client.getStringDetails(notFoundFlagKey, notFoundDefaultValue); @@ -273,7 +273,7 @@ public void the_reason_should_indicate_an_error_and_the_error_code_should_be_fla // type mismatch @When("a string flag with key {string} is evaluated as an integer, with details and a default value {int}") public void a_string_flag_with_key_is_evaluated_as_an_integer_with_details_and_a_default_value(String flagKey, - int defaultValue) { + int defaultValue) { typeErrorFlagKey = flagKey; typeErrorDefaultValue = defaultValue; typeErrorDetails = client.getIntegerDetails(typeErrorFlagKey, typeErrorDefaultValue); 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 cb7770d48..55ddc07cd 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -1,12 +1,9 @@ package dev.openfeature.sdk.providers.memory; 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.Value; +import dev.openfeature.sdk.*; import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.SneakyThrows; @@ -19,16 +16,11 @@ import static dev.openfeature.sdk.Structure.mapToStructure; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; -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.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -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 static org.mockito.Mockito.*; class InMemoryProviderTest { @@ -41,21 +33,16 @@ class InMemoryProviderTest { void beforeEach() { Map> flags = buildFlags(); provider = spy(new InMemoryProvider(flags)); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> {}); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> { + }); OpenFeatureAPI.getInstance().setProviderAndWait(provider); client = OpenFeatureAPI.getInstance().getClient(); provider.updateFlags(flags); provider.updateFlag("addedFlag", Flag.builder() - .variant("on", true) - .variant("off", false) - .defaultVariant("on") - .build()); - } - - @SneakyThrows - @Test - void eventsTest() { - verify(provider, times(2)).emitProviderConfigurationChanged(any()); + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); } @Test @@ -81,9 +68,9 @@ void getDoubleEvaluation() { @Test void getObjectEvaluation() { Value expectedObject = new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100) + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) ))); assertEquals(expectedObject, client.getObjectValue("object-flag", new Value(true))); } @@ -108,7 +95,7 @@ void shouldThrowIfNotInitialized() { InMemoryProvider inMemoryProvider = new InMemoryProvider(new HashMap<>()); // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client - assertThrows(ProviderNotReadyError.class, ()-> inMemoryProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, () -> inMemoryProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); } @SuppressWarnings("unchecked") diff --git a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java b/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java index 41ffbe18f..12fb71b1b 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java @@ -21,7 +21,7 @@ private static void waitForProviderInitializationComplete(Function extractor.apply(OpenFeatureAPI.getInstance()) == provider); + .until(() -> extractor.apply(OpenFeatureAPI.getInstance()).equals(provider)); } public static void setFeatureProvider(String domain, FeatureProvider provider) { diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java index 0b19f82a5..1944fce22 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java @@ -1,28 +1,22 @@ 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.ProviderState; -import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.*; +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 ProviderState state = ProviderState.NOT_READY; private boolean shutDown = false; private int initTimeoutMs = 0; private String name = "test"; private Metadata metadata = () -> name; + private boolean isFatalInitError = false; - @Override - public ProviderState getState() { - return this.state; + public TestEventsProvider() { } public TestEventsProvider(int initTimeoutMs) { @@ -35,8 +29,18 @@ public TestEventsProvider(int initTimeoutMs, boolean initError, String initError this.initErrorMessage = initErrorMessage; } - public TestEventsProvider(ProviderState initialState) { - this.state = initialState; + 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) { @@ -54,14 +58,13 @@ public void shutdown() { @Override public void initialize(EvaluationContext evaluationContext) throws Exception { - if (ProviderState.NOT_READY.equals(state)) { - // wait half the TIMEOUT, otherwise some init/errors can be fired before we add handlers - Thread.sleep(initTimeoutMs); - if (this.initError) { - this.state = ProviderState.ERROR; - throw new GeneralError(initErrorMessage); + // 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); } - this.state = ProviderState.READY; + throw new GeneralError(initErrorMessage); } } @@ -71,32 +74,48 @@ public Metadata getMetadata() { } @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, - EvaluationContext ctx) { - throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'"); + 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) { - throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'"); + 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) { - throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'"); + 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) { - throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'"); + 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 ctx) { - throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'"); + EvaluationContext invocationContext) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(PASSED_IN_DEFAULT) + .reason(Reason.DEFAULT.toString()) + .build(); } -}; \ No newline at end of file +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java index d90359294..dd2d03ca1 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -16,33 +16,41 @@ @UtilityClass public class TestFlagsUtils { + public static final String BOOLEAN_FLAG_KEY = "boolean-flag"; + public static final String STRING_FLAG_KEY = "string-flag"; + public static final String INT_FLAG_KEY = "integer-flag"; + public static final String FLOAT_FLAG_KEY = "float-flag"; + 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"; + /** * Building flags for testing purposes. * @return map of flags */ public static Map> buildFlags() { Map> flags = new HashMap<>(); - flags.put("boolean-flag", Flag.builder() + flags.put(BOOLEAN_FLAG_KEY, Flag.builder() .variant("on", true) .variant("off", false) .defaultVariant("on") .build()); - flags.put("string-flag", Flag.builder() + flags.put(STRING_FLAG_KEY, Flag.builder() .variant("greeting", "hi") .variant("parting", "bye") .defaultVariant("greeting") .build()); - flags.put("integer-flag", Flag.builder() + flags.put(INT_FLAG_KEY, Flag.builder() .variant("one", 1) .variant("ten", 10) .defaultVariant("ten") .build()); - flags.put("float-flag", Flag.builder() + flags.put(FLOAT_FLAG_KEY, Flag.builder() .variant("tenth", 0.1) .variant("half", 0.5) .defaultVariant("half") .build()); - flags.put("object-flag", Flag.builder() + flags.put(OBJECT_FLAG_KEY, Flag.builder() .variant("empty", new HashMap<>()) .variant("template", new Value(mapToStructure(ImmutableMap.of( "showImages", new Value(true), @@ -51,7 +59,7 @@ public static Map> buildFlags() { )))) .defaultVariant("template") .build()); - flags.put("context-aware", Flag.builder() + flags.put(CONTEXT_AWARE_FLAG_KEY, Flag.builder() .variant("internal", "INTERNAL") .variant("external", "EXTERNAL") .defaultVariant("external") @@ -63,7 +71,7 @@ public static Map> buildFlags() { } }) .build()); - flags.put("wrong-flag", Flag.builder() + flags.put(WRONG_FLAG_KEY, Flag.builder() .variant("one", "uno") .variant("two", "dos") .defaultVariant("one") diff --git a/test-harness b/test-harness deleted file mode 160000 index 2d4c63c80..000000000 --- a/test-harness +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2d4c63c800aa3af172cf09176325d93124153cde diff --git a/version.txt b/version.txt index 81c871de4..feaae22ba 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.10.0 +1.13.0