From 49b49dfe2b302b7c8510499b6985116226039467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Thea?= Date: Mon, 7 Aug 2023 15:32:02 -0300 Subject: [PATCH 1/6] Publish dev task (#521) --- .github/workflows/deploy.yml | 30 ++++++++ build.gradle | 136 ++++++++++++++++++++++------------- 2 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..fa1639912 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,30 @@ +name: Internal deploy + +on: + push: + branches: + - development + +jobs: + build-app: + name: Build App + runs-on: ubuntu-latest + env: + ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} + ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Gradle cache + uses: gradle/gradle-build-action@v2.4.2 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 11 + cache: 'gradle' + + - name: Publish + run: ./gradlew publishDev diff --git a/build.gradle b/build.gradle index 8e1e9b273..aab058bc6 100644 --- a/build.gradle +++ b/build.gradle @@ -16,11 +16,11 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.3.0' + splitVersion = '3.3.1-alpha-1' } android { - compileSdkVersion 31 + compileSdk 33 targetCompatibility = '1.8' sourceCompatibility = '1.8' @@ -31,8 +31,8 @@ android { defaultConfig { - minSdkVersion 15 - targetSdkVersion 30 + minSdk 15 + targetSdk 31 multiDexEnabled true consumerProguardFiles 'split-proguard-rules.pro' @@ -159,6 +159,60 @@ dependencies { androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVer" } +def splitPOM = { + name = 'Split Android SDK' + packaging = 'aar' + description = 'Official Split Android SDK' + url = 'https://github.com/splitio/android-client' + + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + + developers { + developer { + id = 'sarrubia' + name = 'Sebastian Arrubia' + email = 'sebastian@split.io' + } + + developer { + id = 'fernando' + name = 'Fernando Martin' + email = 'fernando@split.io' + } + } + + scm { + connection = 'scm:git:git@github.com:splitio/android-client.git' + developerConnection = 'scm:git@github.com:splitio/android-client.git' + url = 'https://github.com/splitio/android-client' + } +} + +def releaseRepo = { + name = "ReleaseRepo" + def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials { + username = ossrhUsername + password = ossrhPassword + } +} + +def devRepo = { + name = "DevelopmentRepo" + url = 'https://splitio.jfrog.io/artifactory/maven-all-virtual' + credentials { + username = System.getenv('ARTIFACTORY_USER') + password = System.getenv('ARTIFACTORY_TOKEN') + } +} + afterEvaluate { android.sourceSets.all { sourceSet -> if (!sourceSet.name.startsWith("test")) { @@ -168,7 +222,6 @@ afterEvaluate { publishing { publications { - project.components.all { print("PRJ NAME: " + name) } release(MavenPublication) { from components.release @@ -177,60 +230,43 @@ afterEvaluate { artifact sourcesJar artifact javadocJar - pom { - name = 'Split Android SDK' - packaging = 'aar' - description = 'Official Split Android SDK' - url = 'https://github.com/splitio/android-client' - - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - - developers { - developer { - id = 'sarrubia' - name = 'Sebastian Arrubia' - email = 'sebastian@split.io' - } - - developer { - id = 'fernando' - name = 'Fernando Martin' - email = 'fernando@split.io' - } - } - - scm { - connection = 'scm:git:git@github.com:splitio/android-client.git' - developerConnection = 'scm:git@github.com:splitio/android-client.git' - url = 'https://github.com/splitio/android-client' - } - } + pom splitPOM repositories { - maven { - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - credentials { - username = ossrhUsername - password = ossrhPassword - } - } + maven releaseRepo + maven devRepo } } + + development(MavenPublication) { + from components.release + + groupId = 'io.split.client' + version = splitVersion + artifact sourcesJar + artifact javadocJar + + pom splitPOM + } } } -} -signing { - sign publishing.publications + task publishRelease(type: PublishToMavenRepository) { + publication = publishing.publications.getByName("release") + repository = publishing.repositories.ReleaseRepo + } + + task publishDev(type: PublishToMavenRepository) { + publication = publishing.publications.getByName("development") + repository = publishing.repositories.DevelopmentRepo + } + + signing { + sign publishing.publications.getByName("release") + } } + task sourcesJar(type: Jar) { archiveClassifier.set("sources") from android.sourceSets.main.java.srcDirs From a32b1ac0576a014c033d6616752915d96529b276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Thea?= Date: Mon, 30 Oct 2023 15:15:08 -0300 Subject: [PATCH 2/6] Flag sets (#547) --- build.gradle | 4 +- .../assets/split_changes_flag_set-0.json | 1 + .../assets/split_changes_flag_set-1.json | 1 + .../assets/split_changes_flag_set-2.json | 1 + .../java/fake/SplitClientStub.java | 20 + .../java/fake/SynchronizerSpyImpl.java | 5 - .../java/helper/IntegrationHelper.java | 30 +- .../sets/FlagSetsEvaluationTest.java | 172 ++++++ .../integration/sets/FlagSetsPollingTest.java | 232 +++++++++ .../sets/FlagSetsStreamingTest.java | 289 +++++++++++ .../integration/streaming/ControlTest.java | 14 +- .../telemetry/TelemetryIntegrationTest.java | 126 ++++- .../userconsent/UserConsentModeDebugTest.kt | 4 +- .../userconsent/UserConsentModeNoneTest.kt | 4 +- .../UserConsentModeOptimizedTest.kt | 4 +- .../java/tests/storage/LoadSplitTaskTest.java | 25 +- .../java/tests/storage/SplitsStorageTest.java | 75 ++- .../AlwaysReturnControlSplitClient.java | 26 +- .../android/client/FeatureFlagFilter.java | 10 + .../split/android/client/FilterBuilder.java | 91 +++- .../split/android/client/FilterGrouper.java | 15 +- .../split/android/client/FlagSetsFilter.java | 6 + .../android/client/FlagSetsFilterImpl.java | 44 ++ .../io/split/android/client/SplitClient.java | 143 +++-- .../client/SplitClientFactoryImpl.java | 12 +- .../split/android/client/SplitClientImpl.java | 20 + .../android/client/SplitFactoryHelper.java | 39 +- .../android/client/SplitFactoryImpl.java | 22 +- .../io/split/android/client/SplitFilter.java | 48 +- .../android/client/SplitManagerImpl.java | 2 + .../io/split/android/client/SyncConfig.java | 22 +- .../split/android/client/api/SplitView.java | 7 +- .../io/split/android/client/dtos/Split.java | 33 ++ .../localhost/LocalhostSplitClient.java | 81 ++- .../localhost/LocalhostSplitFactory.java | 39 +- .../localhost/LocalhostSplitsStorage.java | 35 ++ .../localhost/LocalhostSynchronizer.java | 8 +- .../LocalhostSplitClientContainerImpl.java | 11 +- .../client/service/ServiceConstants.java | 2 + .../executor/SplitTaskExecutionInfo.java | 1 + .../service/executor/SplitTaskFactory.java | 2 +- .../executor/SplitTaskFactoryImpl.java | 59 ++- .../service/http/HttpGeneralException.java | 1 + .../client/service/http/HttpStatus.java | 38 ++ .../splits/FeatureFlagProcessStrategy.java | 71 +++ .../splits/FilterSplitsInCacheTask.java | 40 +- .../client/service/splits/LoadSplitsTask.java | 17 +- .../service/splits/SplitChangeProcessor.java | 78 ++- .../splits/SplitInPlaceUpdateTask.java | 2 +- .../client/service/splits/SplitKillTask.java | 15 +- .../service/splits/SplitsSyncHelper.java | 8 + .../client/service/splits/SplitsSyncTask.java | 8 +- .../sseclient/RetryBackoffCounterTimer.java | 23 +- .../FeatureFlagsSynchronizer.java | 2 - .../FeatureFlagsSynchronizerImpl.java | 14 +- .../service/synchronizer/Synchronizer.java | 2 - .../synchronizer/SynchronizerImpl.java | 11 +- .../synchronizer/WorkManagerWrapper.java | 12 +- .../telemetry/TelemetryTaskFactoryImpl.java | 11 +- .../service/workmanager/SplitsSyncWorker.java | 36 +- .../shared/SplitClientContainerImpl.java | 8 +- .../client/storage/splits/SplitsStorage.java | 5 + .../storage/splits/SplitsStorageImpl.java | 73 +++ .../client/telemetry/model/Config.java | 22 + .../client/telemetry/model/Method.java | 4 + .../telemetry/model/MethodExceptions.java | 44 ++ .../telemetry/model/MethodLatencies.java | 44 ++ .../storage/InMemoryTelemetryStorage.java | 20 +- .../storage/TelemetryConfigProviderImpl.java | 10 +- .../validators/FlagSetsValidatorImpl.java | 102 ++++ .../validators/SplitFilterValidator.java | 35 ++ .../client/validators/TreatmentManager.java | 11 + .../TreatmentManagerFactoryImpl.java | 23 +- .../validators/TreatmentManagerImpl.java | 160 +++++- .../engine/experiments/ParsedSplit.java | 138 ++--- .../engine/experiments/SplitParser.java | 15 +- .../android/client/FilterBuilderTest.java | 99 +++- .../android/client/FilterGrouperTest.java | 36 +- .../client/FlagSetsFilterImplTest.java | 73 +++ .../client/SplitClientImplFlagSetsTest.java | 43 ++ .../android/client/SplitManagerImplTest.java | 51 +- .../split/android/client/SyncConfigTest.java | 12 + .../client/TreatmentManagerTelemetryTest.java | 26 +- .../android/client/TreatmentManagerTest.java | 49 +- .../TreatmentManagerWithFlagSetsTest.java | 490 ++++++++++++++++++ ...LocalhostSplitClientContainerImplTest.java | 16 +- .../service/FilterSplitsInCacheTaskTest.java | 94 +++- .../client/service/LoadSplitsTaskTest.java | 123 +++++ .../client/service/SplitsSyncHelperTest.java | 36 +- .../client/service/SynchronizerTest.java | 8 +- .../splits/SplitChangeProcessorTest.java | 304 +++++++++++ .../RetryBackoffCounterTimerTest.java | 56 +- .../FeatureFlagsSynchronizerImplTest.java | 20 +- .../synchronizer/WorkManagerWrapperTest.java | 9 +- .../SynchronizerImplTelemetryTest.java | 3 +- .../TelemetryConfigBodySerializerTest.java | 6 +- .../TelemetryStatsBodySerializerTest.java | 10 +- .../storage/InMemoryTelemetryStorageTest.java | 36 ++ .../TelemetryConfigProviderImplTest.java | 4 +- .../client/utils/SplitClientImplFactory.java | 14 +- .../validators/FlagSetsValidatorImplTest.java | 104 ++++ .../engine/experiments/SplitParserTest.java | 2 + .../splits/SplitChangeProcessorTest.java | 155 ------ .../io/split/android/helpers/SplitHelper.java | 31 +- 104 files changed, 4182 insertions(+), 616 deletions(-) create mode 100644 src/androidTest/assets/split_changes_flag_set-0.json create mode 100644 src/androidTest/assets/split_changes_flag_set-1.json create mode 100644 src/androidTest/assets/split_changes_flag_set-2.json create mode 100644 src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java create mode 100644 src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java create mode 100644 src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java create mode 100644 src/main/java/io/split/android/client/FeatureFlagFilter.java create mode 100644 src/main/java/io/split/android/client/FlagSetsFilter.java create mode 100644 src/main/java/io/split/android/client/FlagSetsFilterImpl.java create mode 100644 src/main/java/io/split/android/client/service/http/HttpStatus.java create mode 100644 src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java create mode 100644 src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java create mode 100644 src/main/java/io/split/android/client/validators/SplitFilterValidator.java create mode 100644 src/test/java/io/split/android/client/FlagSetsFilterImplTest.java create mode 100644 src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java create mode 100644 src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java create mode 100644 src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java create mode 100644 src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java create mode 100644 src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java delete mode 100644 src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java diff --git a/build.gradle b/build.gradle index aab058bc6..78ec8cda1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.3.1-alpha-1' + splitVersion = '3.4.0-alpha-1' } android { @@ -225,6 +225,7 @@ afterEvaluate { release(MavenPublication) { from components.release + artifactId = 'android-client' groupId = 'io.split.client' version = splitVersion artifact sourcesJar @@ -241,6 +242,7 @@ afterEvaluate { development(MavenPublication) { from components.release + artifactId = 'android-client' groupId = 'io.split.client' version = splitVersion artifact sourcesJar diff --git a/src/androidTest/assets/split_changes_flag_set-0.json b/src/androidTest/assets/split_changes_flag_set-0.json new file mode 100644 index 000000000..93be5fda4 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-0.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602798638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602797638344,"till":1602798638344} diff --git a/src/androidTest/assets/split_changes_flag_set-1.json b/src/androidTest/assets/split_changes_flag_set-1.json new file mode 100644 index 000000000..67f617712 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-1.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602797638344,"algo":2,"configurations":{},"sets":["set_1"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602797638344} diff --git a/src/androidTest/assets/split_changes_flag_set-2.json b/src/androidTest/assets/split_changes_flag_set-2.json new file mode 100644 index 000000000..a96e3e209 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-2.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":-1,"till":1602796638344} diff --git a/src/androidTest/java/fake/SplitClientStub.java b/src/androidTest/java/fake/SplitClientStub.java index b20df3182..d500ef366 100644 --- a/src/androidTest/java/fake/SplitClientStub.java +++ b/src/androidTest/java/fake/SplitClientStub.java @@ -38,6 +38,26 @@ public Map getTreatmentsWithConfig(List featureFlag return null; } + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return null; + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return null; + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return null; + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return null; + } + @Override public void destroy() { diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/src/androidTest/java/fake/SynchronizerSpyImpl.java index b073f066b..88a22b8c3 100644 --- a/src/androidTest/java/fake/SynchronizerSpyImpl.java +++ b/src/androidTest/java/fake/SynchronizerSpyImpl.java @@ -30,11 +30,6 @@ public void loadAndSynchronizeSplits() { mSynchronizer.loadAndSynchronizeSplits(); } - @Override - public void loadSplitsFromCache() { - mSynchronizer.loadSplitsFromCache(); - } - @Override public void loadMySegmentsFromCache() { mSynchronizer.loadMySegmentsFromCache(); diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 243b46dbe..92fcce547 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -2,6 +2,7 @@ import android.content.Context; +import androidx.annotation.Nullable; import androidx.core.util.Pair; import com.google.common.base.Strings; @@ -16,6 +17,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.BlockingQueue; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -257,12 +259,18 @@ public static String splitChangeV2CompressionType0() { "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); } - private static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { + public static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { return "id: vQQ61wzBRO:0:0\n" + "event: message\n" + "data: {\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":"+changeNumber+",\\\"pcn\\\":"+previousChangeNumber+",\\\"c\\\":"+compressionType+",\\\"d\\\":\\\""+compressedPayload+"\\\"}\"}\n"; } + public static String splitKill(String changeNumber, String splitName) { + return "id:cf74eb42-f687-48e4-ad18-af2125110aac\n" + + "event:message\n" + + "data:{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"" + splitName + "\\\"}\"}\n"; + } + /** * Builds a dispatcher with the given responses. * @@ -270,17 +278,17 @@ private static String splitChangeV2(String changeNumber, String previousChangeNu * @return The dispatcher to be used in {@link HttpClientMock} */ public static HttpResponseMockDispatcher buildDispatcher(Map responses) { - return buildDispatcher(responses, Collections.emptyMap()); + return buildDispatcher(responses, null); } /** * Builds a dispatcher with the given responses. * * @param responses The responses to be returned by the dispatcher. The keys are url paths. - * @param streamingResponses The streaming responses to be returned by the dispatcher. The keys are url paths. + * @param streamingQueue The streaming responses to be returned by the dispatcher. * @return The dispatcher to be used in {@link HttpClientMock} */ - public static HttpResponseMockDispatcher buildDispatcher(Map responses, Map streamingResponses) { + public static HttpResponseMockDispatcher buildDispatcher(Map responses, @Nullable BlockingQueue streamingQueue) { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @@ -300,17 +308,11 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @Override public HttpStreamResponseMock getStreamResponse(URI uri) { try { - String path = uri.getPath().replace("/api/", ""); - if (streamingResponses.containsKey(path)) { - return streamingResponses.get(path).onResponse(uri); - } else { - return new HttpStreamResponseMock(200, null); - } + return new HttpStreamResponseMock(200, streamingQueue); } catch (IOException e) { e.printStackTrace(); + return null; } - - return null; } }; } @@ -322,6 +324,10 @@ public interface ResponseClosure { HttpResponseMock onResponse(URI uri, HttpMethod httpMethod, String body); + + static String getSinceFromUri(URI uri) { + return uri.getQuery().split("&")[0].split("=")[1]; + } } /** diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java new file mode 100644 index 000000000..55864f291 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -0,0 +1,172 @@ +package tests.integration.sets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SplitResult; +import io.split.android.client.SyncConfig; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import tests.integration.shared.TestingHelper; + +public class FlagSetsEvaluationTest { + + private final FileHelper fileHelper = new FileHelper(); + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + @Test + public void sdkWithSetsCanOnlyEvaluateSetsConfigured() throws IOException, InterruptedException { + /* + Initialize with set_1 configured. Changes contains 1 flag in set_1 & set_2, and one in set_3. + + Only the flag in set_1 can be evaluated. + */ + SplitClient client = getClient(mContext, DatabaseHelper.getTestDatabase(mContext), "set_1"); + + Map set1Treatments = client.getTreatmentsByFlagSet("set_1", null); + Map set2Treatments = client.getTreatmentsByFlagSet("set_2", null); + Map set3Treatments = client.getTreatmentsByFlagSet("set_3", null); + Map set1Results = client.getTreatmentsWithConfigByFlagSet("set_1", null); + Map set2Results = client.getTreatmentsWithConfigByFlagSet("set_2", null); + Map set3Results = client.getTreatmentsWithConfigByFlagSet("set_3", null); + Map allTreatments = client.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2", "set_3"), null); + Map allTreatmentsWithConfig = client.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2", "set_3"), null); + + assertEquals(1, set1Treatments.size()); + assertEquals(0, set2Treatments.size()); + assertEquals(0, set3Treatments.size()); + assertEquals(1, set1Results.size()); + assertEquals(0, set2Results.size()); + assertEquals(0, set3Results.size()); + assertEquals(1, allTreatments.size()); + assertEquals(1, allTreatmentsWithConfig.size()); + assertTrue(set1Treatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(set1Results.values().stream().noneMatch(t -> t.treatment().equals("control"))); + assertTrue(allTreatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(allTreatmentsWithConfig.values().stream().noneMatch(t -> t.treatment().equals("control"))); + } + + @Test + public void sdkWithoutSetsCanEvaluateAnySet() throws IOException, InterruptedException { + /* + Initialize with no sets configured. Changes contains 1 flag in set_1 & set_2, and one in set_3. + + All flags can be evaluated by sets. + */ + SplitClient client = getClient(mContext, DatabaseHelper.getTestDatabase(mContext)); + + Map set1Treatments = client.getTreatmentsByFlagSet("set_1", null); + Map set2Treatments = client.getTreatmentsByFlagSet("set_2", null); + Map set3Treatments = client.getTreatmentsByFlagSet("set_3", null); + Map set1Results = client.getTreatmentsWithConfigByFlagSet("set_1", null); + Map set2Results = client.getTreatmentsWithConfigByFlagSet("set_2", null); + Map set3Results = client.getTreatmentsWithConfigByFlagSet("set_3", null); + Map allTreatments = client.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2", "set_3"), null); + Map allTreatmentsWithConfig = client.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2", "set_3"), null); + + assertEquals(1, set1Treatments.size()); + assertEquals(1, set2Treatments.size()); + assertEquals(1, set3Treatments.size()); + assertEquals(1, set1Results.size()); + assertEquals(1, set2Results.size()); + assertEquals(1, set3Results.size()); + assertTrue(set1Treatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(set2Treatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(set3Treatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(set1Results.values().stream().noneMatch(t -> t.treatment().equals("control"))); + assertTrue(set2Results.values().stream().noneMatch(t -> t.treatment().equals("control"))); + assertTrue(set3Results.values().stream().noneMatch(t -> t.treatment().equals("control"))); + assertTrue(allTreatments.values().stream().noneMatch(t -> t.equals("control"))); + assertTrue(allTreatmentsWithConfig.values().stream().noneMatch(t -> t.treatment().equals("control"))); + } + + private SplitClient getClient( + Context mContext, + SplitRoomDatabase splitRoomDatabase, + String... sets) throws IOException, InterruptedException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .trafficType("client") + .enableDebug() + .impressionsRefreshRate(1000) + .impressionsCountersRefreshRate(1000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .build()) + .featuresRefreshRate(2) + .streamingEnabled(false) + .eventFlushInterval(1000) + .build(); + + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + String since = getSinceFromUri(uri); + + if (since.equals("-1")) { + return new HttpResponseMock(200, loadSplitChangeWithSet(2)); + } else { + return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(1602796638344L, 1602796638344L)); + } + }); + + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses); + + SplitFactory factory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + + CountDownLatch readyLatch = new CountDownLatch(1); + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + + boolean readyAwait = readyLatch.await(5, TimeUnit.SECONDS); + + if (readyAwait) { + return client; + } else { + return null; + } + } + + private String loadSplitChangeWithSet(int setsCount) { + String change = fileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + + return Json.toJson(parsedChange); + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java new file mode 100644 index 000000000..ccb4e94c1 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -0,0 +1,232 @@ +package tests.integration.sets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SyncConfig; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +public class FlagSetsPollingTest { + + private final FileHelper fileHelper = new FileHelper(); + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch hitsLatch; + private CountDownLatch firstChangeLatch; + private CountDownLatch secondChangeLatch; + private CountDownLatch thirdChangeLatch; + private SplitRoomDatabase mRoomDb; + private volatile String mSplitChangesUri; + + @Before + public void setUp() throws Exception { + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + + hitsLatch = new CountDownLatch(3); + firstChangeLatch = new CountDownLatch(1); + secondChangeLatch = new CountDownLatch(1); + thirdChangeLatch = new CountDownLatch(1); + } + + @Test + public void featureFlagIsUpdatedAccordingToSetsWhenTheyAreConfigured() throws IOException, InterruptedException { + + // 1. Initialize a factory with polling and sets set_1 & set_2 configured. + createFactory(mContext, mRoomDb, "set_1", "set_2"); + + // 2. Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + // -> only one feature flag should be added + boolean awaitFirst = firstChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int firstSize = mRoomDb.splitDao().getAll().size(); + boolean firstSetsCorrect = mRoomDb.splitDao().getAll().get(0).getBody().contains("[\"set_1\",\"set_2\"]"); + + // 3. Receive split change with 1 split belonging to set_1 only + // -> the feature flag should be updated + boolean awaitSecond = secondChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int secondSize = mRoomDb.splitDao().getAll().size(); + boolean secondSetsCorrect = mRoomDb.splitDao().getAll().get(0).getBody().contains("[\"set_1\"]"); + + // 4. Receive split change with 1 split belonging to set_3 only + // -> the feature flag should be removed + boolean awaitThird = thirdChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int thirdSize = mRoomDb.splitDao().getAll().size(); + + boolean awaitHits = hitsLatch.await(120, TimeUnit.SECONDS); + + assertEquals(1, firstSize); + assertEquals(1, secondSize); + assertEquals(0, thirdSize); + assertTrue(awaitFirst); + assertTrue(awaitSecond); + assertTrue(awaitThird); + assertTrue(firstSetsCorrect); + assertTrue(secondSetsCorrect); + + assertTrue(awaitHits); + } + + @Test + public void featureFlagSetsAreIgnoredWhenSetsAreNotConfigured() throws IOException, InterruptedException { + + // 1. Initialize a factory with polling and sets set_1 & set_2 configured. + createFactory(mContext, mRoomDb); + + // 2. Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + // -> only one feature flag should be added + boolean awaitFirst = firstChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + int firstSize = mRoomDb.splitDao().getAll().size(); + List firstEntities = mRoomDb.splitDao().getAll(); + boolean firstSetsCorrect = firstEntities.get(0).getBody().contains("[\"set_1\",\"set_2\"]") && + firstEntities.get(1).getBody().contains("[\"set_3\"]"); + + // 3. Receive split change with 1 split belonging to set_1 only + // -> the feature flag should be updated + boolean awaitSecond = secondChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + int secondSize = mRoomDb.splitDao().getAll().size(); + List secondEntities = mRoomDb.splitDao().getAll(); + String body0 = secondEntities.get(0).getBody(); + String body1 = secondEntities.get(1).getBody(); + boolean secondSetsCorrect = body1.contains("[\"set_1\"]") && + body1.contains("\"name\":\"workm\",") && + body0.contains("\"name\":\"workm_set_3\",") && + body0.contains("[\"set_3\"]"); + + Logger.w("body0: " + body0); + Logger.w("body1: " + body1); + + // 4. Receive split change with 1 split belonging to set_3 only + // -> the feature flag should be removed + boolean awaitThird = thirdChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + List thirdEntities = mRoomDb.splitDao().getAll(); + int thirdSize = thirdEntities.size(); + String body30 = thirdEntities.get(0).getBody(); + String body31 = thirdEntities.get(1).getBody(); + boolean thirdSetsCorrect = body31.contains("[\"set_3\"]") && + body31.contains("\"name\":\"workm\",") && + body30.contains("\"name\":\"workm_set_3\",") && + body30.contains("[\"set_3\"]"); + + boolean awaitHits = hitsLatch.await(120, TimeUnit.SECONDS); + + assertEquals(2, firstSize); + assertEquals(2, secondSize); + assertEquals(2, thirdSize); + assertTrue(awaitFirst); + assertTrue(awaitSecond); + assertTrue(awaitThird); + assertTrue(firstSetsCorrect); + assertTrue(secondSetsCorrect); + assertTrue(thirdSetsCorrect); + + assertTrue(awaitHits); + } + + @Test + public void queryStringIsBuiltCorrectlyWhenSetsAreConfigured() throws IOException, InterruptedException { + createFactory(mContext, mRoomDb, "set_x", "set_x", "set_3", "set_2", "set_3", "set_ww", "invalid+"); + + boolean awaitFirst = firstChangeLatch.await(5, TimeUnit.SECONDS); + + String uri = mSplitChangesUri; + + assertTrue(awaitFirst); + assertEquals("https://sdk.split.io/api/splitChanges?since=-1&sets=set_2,set_3,set_ww,set_x", uri); + } + + private SplitFactory createFactory( + Context mContext, + SplitRoomDatabase splitRoomDatabase, + String... sets) throws IOException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .trafficType("client") + .enableDebug() + .impressionsRefreshRate(1000) + .impressionsCountersRefreshRate(1000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .addSplitFilter(SplitFilter.byName(Arrays.asList("workm", "workm_set_3"))) // added to test that this filter is ignored + .addSplitFilter(SplitFilter.byPrefix(Collections.singletonList("pref"))) // added to test that this filter is ignored + .build()) + .featuresRefreshRate(2) + .streamingEnabled(false) + .eventFlushInterval(1000) + .build(); + + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + mSplitChangesUri = uri.toString(); + String since = getSinceFromUri(uri); + hitsLatch.countDown(); + if (since.equals("-1")) { + firstChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(2)); + } else if (since.equals("1602796638344")) { + secondChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(1)); + } else { + thirdChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(0)); + } + }); + + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses); + + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + } + + private String loadSplitChangeWithSet(int setsCount) { + String change = fileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + + return Json.toJson(parsedChange); + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java new file mode 100644 index 000000000..d61a46e06 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java @@ -0,0 +1,289 @@ +package tests.integration.sets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static helper.IntegrationHelper.splitChangeV2; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SyncConfig; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.logger.Logger; +import tests.integration.shared.TestingHelper; + +public class FlagSetsStreamingTest { + + // workm with set_3, set_4 + private static final String splitChange5 = splitChangeV2("5", "4", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyIsInNldF80Il0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0"); + // workm with no sets + private static final String splitChange4None = splitChangeV2("4", "3", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6W10sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); + // workm with set_3 + private static final String splitChange4 = splitChangeV2("4", "3", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjQsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + // workm with set_1 + private static final String splitChange3 = splitChangeV2("3", "2", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjMsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + // workm with set_1, set_2 + private static final String splitChange2 = splitChangeV2("2", "1", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjIsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSIsInNldF8yIl0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0"); + // mauro_java with no sets + private static final String noSetsSplitChange = splitChangeV2("2", "1", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTYwMjc5OTYzODM0NCwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJzZXRzIjpbXSwiY29uZGl0aW9ucyI6W3siY29uZGl0aW9uVHlwZSI6IldISVRFTElTVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJtYXRjaGVyVHlwZSI6IldISVRFTElTVCIsIm5lZ2F0ZSI6ZmFsc2UsIndoaXRlbGlzdE1hdGNoZXJEYXRhIjp7IndoaXRlbGlzdCI6WyJhZG1pbiIsIm1hdXJvIiwibmljbyJdfX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoid2hpdGVsaXN0ZWQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6Im1hdXItMiJ9fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG1hdXItMiJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private final AtomicInteger mSplitChangesHits = new AtomicInteger(0); + private SplitRoomDatabase mRoomDb; + + @Before + public void setUp() { + mSplitChangesHits.set(0); + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + } + + @Test + public void sdkWithoutSetsConfiguredDoesNotExcludeUpdates() throws IOException, InterruptedException { + // 1. Initialize a factory with streaming enabled and no sets. + LinkedBlockingDeque mStreamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, mStreamingData); + + int initialSplitsSize = mRoomDb.splitDao().getAll().size(); + CountDownLatch updateLatch = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + + // 2. Receive notification with new feature flag with no sets. + // 3. Assert that the update is processed and the split is stored. + pushToStreaming(mStreamingData, noSetsSplitChange); + boolean updateAwait = updateLatch.await(5, TimeUnit.SECONDS); + + assertTrue(updateAwait); + assertEquals(0, initialSplitsSize); + assertEquals(1, mRoomDb.splitDao().getAll().size()); + } + + @Test + public void sdkWithSetsConfiguredDeletedDueToEmptySets() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 1. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> flag is added to the storage + boolean firstChange = processUpdate(readyClient, streamingData, splitChange2, "\"sets\":[\"set_1\",\"set_2\"]", "\"name\":\"workm\""); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1"] + // -> flag is updated in storage + boolean secondChange = processUpdate(readyClient, streamingData, splitChange3, "\"sets\":[\"set_1\"]", "\"name\":\"workm\""); + + // 3. Receive a SPLIT_UPDATE with "sets":[] + // -> flag is removed from storage + boolean thirdChange = processUpdate(readyClient, streamingData, splitChange4None); + + assertTrue(firstChange); + assertTrue(secondChange); + assertTrue(thirdChange); + } + + @Test + public void sdkWithSetsConfiguredDeletedDueToNonMatchingSets() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 1. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> workm is added to the storage + boolean firstChange = processUpdate(readyClient, streamingData, splitChange2, "\"sets\":[\"set_1\",\"set_2\"]", "\"name\":\"workm\""); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1"] + // -> workm sets are updated to set_1 only + boolean secondChange = processUpdate(readyClient, streamingData, splitChange3, "\"sets\":[\"set_1\"]", "\"name\":\"workm\""); + + // 3. Receive a SPLIT_UPDATE with "sets":["set_3"] + // -> workm is removed from the storage + boolean thirdChange = processUpdate(readyClient, streamingData, splitChange4); + + // 4. Receive a SPLIT_UPDATE with "sets":["set_3", "set_4"] + // -> workm is not added to the storage + boolean fourthChange = processUpdate(readyClient, streamingData, splitChange5); + + assertTrue(firstChange); + assertTrue(secondChange); + assertTrue(thirdChange); + assertTrue(fourthChange); + } + + @Test + public void sdkWithSetsReceivesSplitKill() throws IOException, InterruptedException { + + // 1. Initialize a factory with set_1 & set_2 sets configured. + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> flag is added to the storage + CountDownLatch firstUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(firstUpdate)); + pushToStreaming(streamingData, splitChange2); + boolean firstUpdateAwait = firstUpdate.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + boolean firstUpdateStored = entities.size() == 1 && entities.get(0).getBody().contains("\"sets\":[\"set_1\",\"set_2\"]") && + entities.get(0).getBody().contains("\"killed\":false") && + entities.get(0).getBody().contains("\"name\":\"workm\""); + + // 3. Receive a SPLIT_KILL for flag + // -> flag is updated in storage + CountDownLatch secondUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(secondUpdate)); + pushToStreaming(streamingData, IntegrationHelper.splitKill("5", "workm")); + boolean secondUpdateAwait = secondUpdate.await(5, TimeUnit.SECONDS); + entities = mRoomDb.splitDao().getAll(); + boolean secondUpdateStored = entities.size() == 1 && entities.get(0).getBody().contains("\"killed\":true") && + entities.get(0).getBody().contains("\"name\":\"workm\""); + + // 4. A fetch is triggered due to the SPLIT_KILL + boolean correctAmountOfChanges = mSplitChangesHits.get() == 3; + + assertTrue(firstUpdateAwait); + assertTrue(firstUpdateStored); + assertTrue(secondUpdateAwait); + assertTrue(secondUpdateStored); + assertTrue(correctAmountOfChanges); + } + + @Test + public void sdkWithSetsReceivesSplitKillForNonExistingFeatureFlag() throws IOException, InterruptedException { + + // 1. Initialize a factory with set_1 & set_2 sets configured. + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + int initialEntities = mRoomDb.splitDao().getAll().size(); + + // 2. Receive a SPLIT_KILL; storage is not modified since flag is not present. + CountDownLatch firstUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(firstUpdate)); + int initialChangesHits = mSplitChangesHits.get(); + pushToStreaming(streamingData, IntegrationHelper.splitKill("2", "workm")); + boolean firstUpdateAwait = firstUpdate.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + boolean firstUpdateStored = entities.isEmpty(); + + // 3. A fetch is triggered due to the SPLIT_KILL + int finalChangesHits = mSplitChangesHits.get(); + + assertFalse(firstUpdateAwait); + assertTrue(firstUpdateStored); + assertEquals(initialEntities, 0); + assertTrue(finalChangesHits > initialChangesHits); + } + + @Nullable + private SplitClient getReadyClient( + Context mContext, + SplitRoomDatabase splitRoomDatabase, + BlockingQueue streamingData, + String... sets) throws IOException, InterruptedException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .trafficType("client") + .enableDebug() + .impressionsRefreshRate(1000) + .impressionsCountersRefreshRate(1000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .build()) + .featuresRefreshRate(2) + .streamingEnabled(true) + .eventFlushInterval(1000) + .build(); + CountDownLatch authLatch = new CountDownLatch(1); + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + mSplitChangesHits.incrementAndGet(); + return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(-1, 1)); + }); + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put("v2/auth", (uri, httpMethod, body) -> { + authLatch.countDown(); + return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); + }); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses, streamingData); + + SplitFactory splitFactory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + + CountDownLatch readyLatch = new CountDownLatch(1); + SplitClient client = splitFactory.client(); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean await = readyLatch.await(5, TimeUnit.SECONDS); + boolean authAwait = authLatch.await(5, TimeUnit.SECONDS); + TestingHelper.pushKeepAlive(streamingData); + + return (await && authAwait) ? client : null; + } + + private static void pushToStreaming(LinkedBlockingDeque streamingData, String message) throws InterruptedException { + try { + streamingData.put(message + "" + "\n"); + + Logger.d("Pushed message: " + message); + } catch (InterruptedException ignored) { + } + } + + private boolean processUpdate(SplitClient client, LinkedBlockingDeque streamingData, String splitChange, String... expectedContents) throws InterruptedException { + CountDownLatch updateLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + pushToStreaming(streamingData, splitChange); + boolean updateAwaited = updateLatch.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + + if (expectedContents == null || expectedContents.length == 0) { + return updateAwaited && entities.isEmpty(); + } + + boolean contentMatches = true; + for (String expected : expectedContents) { + contentMatches = contentMatches && entities.size() == 1 && entities.get(0).getBody().contains(expected); + } + + return updateAwaited && contentMatches; + } +} diff --git a/src/androidTest/java/tests/integration/streaming/ControlTest.java b/src/androidTest/java/tests/integration/streaming/ControlTest.java index b817e20ed..200fb5138 100644 --- a/src/androidTest/java/tests/integration/streaming/ControlTest.java +++ b/src/androidTest/java/tests/integration/streaming/ControlTest.java @@ -92,7 +92,8 @@ public void controlNotification() throws IOException, InterruptedException { CountDownLatch readyLatch = new CountDownLatch(1); - TestingHelper.TestEventTask updateTask = TestingHelper.testTask(new CountDownLatch(1), "CONTROL notif update task"); + CountDownLatch updateLatch = new CountDownLatch(3); + SplitEventTaskHelper updateTask = new SplitEventTaskHelper(updateLatch); HttpClientMock httpClientMock = new HttpClientMock(createBasicResponseDispatcher()); @@ -133,19 +134,17 @@ public void controlNotification() throws IOException, InterruptedException { pushControl("STREAMING_RESUMED"); synchronizerSpy.stopPeriodicFetchLatch.await(10, TimeUnit.SECONDS); - updateTask.mLatch = new CountDownLatch(1); pushMySegmentsUpdatePayload("new_segment"); - updateTask.mLatch.await(10, TimeUnit.SECONDS); + updateLatch.await(10, TimeUnit.SECONDS); String treatmentEnabled = mClient.getTreatment(splitName); //Enable streaming, push a new my segments payload update and check data again - updateTask.mLatch = new CountDownLatch(1); + updateLatch = new CountDownLatch(1); pushControl("STREAMING_DISABLED"); - updateTask.mLatch.await(5, TimeUnit.SECONDS); + updateLatch.await(5, TimeUnit.SECONDS); pushMySegmentsUpdatePayload("new_segment"); - sleep(1000); - + updateLatch.await(5, TimeUnit.SECONDS); String treatmentDisabled = mClient.getTreatment(splitName); assertTrue(telemetryStorage.popStreamingEvents().stream().anyMatch(event -> { @@ -154,7 +153,6 @@ public void controlNotification() throws IOException, InterruptedException { } return false; })); - assertEquals(1, telemetryStorage.popTokenRefreshes()); Assert.assertEquals("on", treatmentReady); Assert.assertEquals("on", treatmentPaused); Assert.assertEquals("free", treatmentEnabled); diff --git a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java index 414c3df83..967ca15bc 100644 --- a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java +++ b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java @@ -2,6 +2,7 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; import android.content.Context; @@ -11,6 +12,7 @@ import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -28,6 +30,8 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SyncConfig; import io.split.android.client.api.Key; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; @@ -41,7 +45,6 @@ import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.Json; -import io.split.android.client.utils.logger.Logger; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -79,12 +82,16 @@ public void telemetryInitTest() { @Test public void telemetryEvaluationLatencyTest() { - initializeClient(false); + initializeClient(false, "a", "b"); client.getTreatment("test_split"); client.getTreatments(Arrays.asList("test_split", "test_split_2"), null); client.getTreatmentWithConfig("test_split", null); client.getTreatmentsWithConfig(Arrays.asList("test_split", "test_split_2"), null); client.track("test_traffic_type", "test_split"); + client.getTreatmentsByFlagSet("a", null); + client.getTreatmentsByFlagSets(Arrays.asList("a", "b"), null); + client.getTreatmentsWithConfigByFlagSet("a", null); + client.getTreatmentsWithConfigByFlagSets(Arrays.asList("a", "b"), null); MethodLatencies methodLatencies = mTelemetryStorage.popLatencies(); assertFalse(methodLatencies.getTreatment().stream().allMatch(aLong -> aLong == 0L)); @@ -92,6 +99,60 @@ public void telemetryEvaluationLatencyTest() { assertFalse(methodLatencies.getTreatmentWithConfig().stream().allMatch(aLong -> aLong == 0L)); assertFalse(methodLatencies.getTreatmentsWithConfig().stream().allMatch(aLong -> aLong == 0L)); assertFalse(methodLatencies.getTrack().stream().allMatch(aLong -> aLong == 0L)); + assertFalse(methodLatencies.getTreatmentsByFlagSet().stream().allMatch(aLong -> aLong == 0L)); + assertFalse(methodLatencies.getTreatmentsByFlagSets().stream().allMatch(aLong -> aLong == 0L)); + assertFalse(methodLatencies.getTreatmentsWithConfigByFlagSet().stream().allMatch(aLong -> aLong == 0L)); + assertFalse(methodLatencies.getTreatmentsWithConfigByFlagSets().stream().allMatch(aLong -> aLong == 0L)); + } + + @Test + public void evaluationByFlagsInfoIsInPayload() throws InterruptedException { + CountDownLatch metricsLatch = new CountDownLatch(1); + AtomicReference metricsPayload = new AtomicReference<>(); + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) { + String path = request.getPath(); + if (path.contains("/mySegments")) { + return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + } else if (path.contains("/splitChanges")) { + long changeNumber = -1; + return new MockResponse().setResponseCode(200) + .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + } else if (path.contains("/events/bulk")) { + return new MockResponse().setResponseCode(200); + } else if (path.contains("metrics/usage")) { + metricsPayload.set(request.getBody().readUtf8()); + metricsLatch.countDown(); + return new MockResponse().setResponseCode(200); + } else if (path.contains("metrics")) { + return new MockResponse().setResponseCode(200); + } else if (path.contains("auth")) { + return new MockResponse().setResponseCode(401); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + + mWebServer.setDispatcher(dispatcher); + + initializeClient(false, "a", "b"); + client.getTreatmentsByFlagSet("a", null); + client.getTreatmentsByFlagSets(Arrays.asList("a", "b"), null); + client.getTreatmentsWithConfigByFlagSet("a", null); + client.getTreatmentsWithConfigByFlagSets(Arrays.asList("a", "b"), null); + + boolean await = metricsLatch.await(10, TimeUnit.SECONDS); + + assertTrue(await); + assertTrue(metricsPayload.get().contains("\"tf\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]")); + assertTrue(metricsPayload.get().contains("\"tfs\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]")); + assertTrue(metricsPayload.get().contains("\"tcf\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]")); + assertTrue(metricsPayload.get().contains("\"tcfs\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]")); + assertTrue(metricsPayload.get().contains("\"tcf\":0,\"tcfs\":0")); + assertTrue(metricsPayload.get().contains("\"tf\":0,\"tfs\":0")); } @Test @@ -195,11 +256,53 @@ public void recordSessionLength() throws InterruptedException { assertTrue(sessionLength > 0); } - private void initializeClient(boolean streamingEnabled) { + @Test + public void flagSetsAreIncludedInPayload() throws InterruptedException { + CountDownLatch sseLatch = new CountDownLatch(1); + CountDownLatch metricsLatch = new CountDownLatch(2); + AtomicReference metricsPayload = new AtomicReference<>(); + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) { + String path = request.getPath(); + if (path.contains("/mySegments")) { + return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + } else if (path.contains("/splitChanges")) { + long changeNumber = -1; + return new MockResponse().setResponseCode(200) + .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + } else if (path.contains("/events/bulk")) { + return new MockResponse().setResponseCode(200); + } else if (path.contains("metrics/usage")) { + metricsLatch.countDown(); + return new MockResponse().setResponseCode(200); + } else if (path.contains("metrics")) { + metricsPayload.set(request.getBody().readUtf8()); + return new MockResponse().setResponseCode(200); + } else if (path.contains("auth")) { + sseLatch.countDown(); + return new MockResponse().setResponseCode(401); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + + mWebServer.setDispatcher(dispatcher); + + initializeClient(false, "a", "_b", "a", "a", "c", "d", "_d"); + metricsLatch.await(20, TimeUnit.SECONDS); + String s = metricsPayload.get(); + assertTrue(s.contains("\"fsI\":4")); + assertTrue(s.contains("\"fsT\":7")); + } + + private void initializeClient(boolean streamingEnabled, String ... sets) { insertSplitsFromFileIntoDB(); CountDownLatch countDownLatch = new CountDownLatch(1); - client = getTelemetrySplitFactory(mWebServer, streamingEnabled).client(); + client = getTelemetrySplitFactory(mWebServer, streamingEnabled, sets).client(); TestingHelper.TestEventTask readyFromCacheTask = new TestingHelper.TestEventTask(countDownLatch); client.on(SplitEvent.SDK_READY, readyFromCacheTask); @@ -211,7 +314,7 @@ private void initializeClient(boolean streamingEnabled) { } } - private SplitFactory getTelemetrySplitFactory(MockWebServer webServer, boolean streamingEnabled) { + private SplitFactory getTelemetrySplitFactory(MockWebServer webServer, boolean streamingEnabled, String... sets) { final String url = webServer.url("/").url().toString(); ServiceEndpoints endpoints = ServiceEndpoints.builder() .eventsEndpoint(url) @@ -219,7 +322,7 @@ private SplitFactory getTelemetrySplitFactory(MockWebServer webServer, boolean s .sseAuthServiceEndpoint(url) .apiEndpoint(url).eventsEndpoint(url).build(); - SplitClientConfig config = new TestableSplitConfigBuilder() + TestableSplitConfigBuilder builder = new TestableSplitConfigBuilder() .serviceEndpoints(endpoints) .enableDebug() .telemetryRefreshRate(10) @@ -228,8 +331,15 @@ private SplitFactory getTelemetrySplitFactory(MockWebServer webServer, boolean s .impressionsRefreshRate(9999) .readTimeout(3000) .streamingEnabled(streamingEnabled) - .shouldRecordTelemetry(true) - .build(); + .shouldRecordTelemetry(true); + + if (sets != null && sets.length > 0) { + builder.syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .build()); + } + + SplitClientConfig config = builder.build(); mTelemetryStorage = StorageFactory.getTelemetryStorage(true); return IntegrationHelper.buildFactory( diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt index b162d2a54..4c0d9eb02 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt @@ -231,7 +231,7 @@ class UserConsentModeDebugTest { } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit+=1 - return getSplitsMockResponse("", "") + return getSplitsMockResponse("") } return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999, 99999999)) } else if (uri.path.contains("/testImpressions/bulk")) { @@ -259,7 +259,7 @@ class UserConsentModeDebugTest { } } - private fun getSplitsMockResponse(since: String, till: String): HttpResponseMock { + private fun getSplitsMockResponse(since: String): HttpResponseMock { return HttpResponseMock(200, loadSplitChanges()) } diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt index dddec5f6a..106bfd65b 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt @@ -232,7 +232,7 @@ class UserConsentModeNoneTest { } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit+=1 - return getSplitsMockResponse("", "") + return getSplitsMockResponse("") } return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999, 99999999)) } else if (uri.path.contains("/testImpressions/bulk")) { @@ -260,7 +260,7 @@ class UserConsentModeNoneTest { } } - private fun getSplitsMockResponse(since: String, till: String): HttpResponseMock { + private fun getSplitsMockResponse(since: String): HttpResponseMock { return HttpResponseMock(200, loadSplitChanges()) } diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt index 4a6ab033b..28e9fdb77 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt @@ -242,7 +242,7 @@ class UserConsentModeOptimizedTest { } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit+=1 - return getSplitsMockResponse("", "") + return getSplitsMockResponse("") } return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999, 99999999)) } else if (uri.path.contains("/testImpressions/bulk")) { @@ -270,7 +270,7 @@ class UserConsentModeOptimizedTest { } } - private fun getSplitsMockResponse(since: String, till: String): HttpResponseMock { + private fun getSplitsMockResponse(since: String): HttpResponseMock { return HttpResponseMock(200, loadSplitChanges()) } diff --git a/src/androidTest/java/tests/storage/LoadSplitTaskTest.java b/src/androidTest/java/tests/storage/LoadSplitTaskTest.java index 745586ca9..e919e7c36 100644 --- a/src/androidTest/java/tests/storage/LoadSplitTaskTest.java +++ b/src/androidTest/java/tests/storage/LoadSplitTaskTest.java @@ -53,9 +53,9 @@ public void setUp() { } @Test - public void execute() { + public void executeWithoutQueryString() { - SplitTask task = new LoadSplitsTask(mSplitsStorage); + SplitTask task = new LoadSplitsTask(mSplitsStorage, null); SplitTaskExecutionInfo result = task.execute(); Split split0 = mSplitsStorage.get("split-0"); @@ -68,5 +68,26 @@ public void execute() { Assert.assertNotNull(split2); Assert.assertNotNull(split3); Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + Assert.assertEquals(9999L, mSplitsStorage.getTill()); + Assert.assertEquals("", mSplitsStorage.getSplitsFilterQueryString()); + } + + @Test + public void executeWithQueryString() { + + SplitTask task = new LoadSplitsTask(mSplitsStorage, "sets=set1"); + SplitTaskExecutionInfo result = task.execute(); + + Split split0 = mSplitsStorage.get("split-0"); + Split split1 = mSplitsStorage.get("split-1"); + Split split2 = mSplitsStorage.get("split-2"); + Split split3 = mSplitsStorage.get("split-3"); + Assert.assertNull(split0); + Assert.assertNull(split1); + Assert.assertNull(split2); + Assert.assertNull(split3); + Assert.assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + Assert.assertEquals(-1L, mSplitsStorage.getTill()); + Assert.assertEquals("sets=set1", mSplitsStorage.getSplitsFilterQueryString()); } } diff --git a/src/androidTest/java/tests/storage/SplitsStorageTest.java b/src/androidTest/java/tests/storage/SplitsStorageTest.java index 7fc36f8d9..1669c1660 100644 --- a/src/androidTest/java/tests/storage/SplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/SplitsStorageTest.java @@ -11,8 +11,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -32,15 +34,15 @@ public class SplitsStorageTest { private static final Long INITIAL_CHANGE_NUMBER = 9999L; private static final String JSON_SPLIT_TEMPLATE = "{\"name\":\"%s\", \"changeNumber\": %d}"; - private static final String JSON_SPLIT_WITH_TRAFFIC_TYPE_TEMPLATE = "{\"name\":\"%s\", \"changeNumber\": %d, \"trafficTypeName\":\"%s\"}"; + private static final String JSON_SPLIT_WITH_TRAFFIC_TYPE_TEMPLATE = "{\"name\":\"%s\", \"changeNumber\": %d, \"trafficTypeName\":\"%s\", \"sets\":[\"%s\"]}"; + private SplitRoomDatabase mRoomDb; - private Context mContext; private SplitsStorage mSplitsStorage; @Before public void setUp() { - mContext = InstrumentationRegistry.getInstrumentation().getContext(); - mRoomDb = DatabaseHelper.getTestDatabase(mContext); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mRoomDb = DatabaseHelper.getTestDatabase(context); mRoomDb.clearAllTables(); List entities = new ArrayList<>(); for (int i = 0; i < 4; i++) { @@ -349,7 +351,63 @@ public void loadedFromStorageTrafficTypesAreCorrectlyUpdated() { Assert.assertTrue(mSplitsStorage.isValidTrafficType("test_type_2")); } + @Test + public void flagSetsAreUpdatedWhenCallingLoadLocal() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList( + newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), + newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")), + newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")), + newSplitEntity("split_test_4", "test_type_2", Collections.singleton("set_1")))); + + mSplitsStorage.loadLocal(); + + Assert.assertEquals(new HashSet<>(Arrays.asList("split_test", "split_test_4")), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); + Assert.assertEquals(new HashSet<>(Arrays.asList("split_test_2", "split_test_3")), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2"))); + } + + @Test + public void flagSetsAreRemovedWhenUpdating() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList( + newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), + newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")), + newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); + Set initialSet2 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2")); + + mSplitsStorage.update(new ProcessedSplitChange( + Collections.singletonList(newSplit("split_test", Status.ACTIVE, "test_type")), Collections.emptyList(), + 1L, 0L)); + + Assert.assertFalse(initialSet1.isEmpty()); + Assert.assertEquals(Collections.emptySet(), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); + Assert.assertEquals(initialSet2, mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2"))); + } + + @Test + public void updateWithoutChecksRemovesFromFlagSet() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); + Set initialSet2 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2")); + + mSplitsStorage.updateWithoutChecks(newSplit("split_test", Status.ACTIVE, "test_type")); + + Assert.assertFalse(initialSet1.isEmpty()); + Assert.assertEquals(Collections.emptySet(), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); + Assert.assertEquals(initialSet2, mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2"))); + } + private Split newSplit(String name, Status status, String trafficType) { + return newSplit(name, status, trafficType, Collections.emptySet()); + } + + private Split newSplit(String name, Status status, String trafficType, Set sets) { Split split = new Split(); split.name = name; split.status = status; @@ -358,13 +416,20 @@ private Split newSplit(String name, Status status, String trafficType) { } else { split.trafficTypeName = "custom"; } + split.sets = sets; + return split; } private static SplitEntity newSplitEntity(String name, String trafficType) { + return newSplitEntity(name, trafficType, Collections.emptySet()); + } + + private static SplitEntity newSplitEntity(String name, String trafficType, Set sets) { SplitEntity entity = new SplitEntity(); + String setsString = String.join(",", sets); entity.setName(name); - entity.setBody(String.format(JSON_SPLIT_WITH_TRAFFIC_TYPE_TEMPLATE, name, INITIAL_CHANGE_NUMBER, trafficType)); + entity.setBody(String.format(JSON_SPLIT_WITH_TRAFFIC_TYPE_TEMPLATE, name, INITIAL_CHANGE_NUMBER, trafficType, setsString)); return entity; } diff --git a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index 70b6356f2..2275622fb 100644 --- a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -6,6 +6,8 @@ import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.grammar.Treatments; + +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,7 +17,7 @@ * Useful for testing * */ -public class AlwaysReturnControlSplitClient implements io.split.android.client.SplitClient { +public class AlwaysReturnControlSplitClient implements SplitClient { @Override public String getTreatment(String featureFlagName) { @@ -58,6 +60,26 @@ public SplitResult getTreatmentWithConfig(String featureFlagName, Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return Collections.emptyMap(); + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return Collections.emptyMap(); + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return Collections.emptyMap(); + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return Collections.emptyMap(); + } + @Override public boolean setAttribute(String attributeName, Object value) { return true; @@ -149,6 +171,4 @@ public boolean track(String eventType, Map properties) { public boolean track(String eventType, double value, Map properties) { return false; } - - } diff --git a/src/main/java/io/split/android/client/FeatureFlagFilter.java b/src/main/java/io/split/android/client/FeatureFlagFilter.java new file mode 100644 index 000000000..fb4dc7764 --- /dev/null +++ b/src/main/java/io/split/android/client/FeatureFlagFilter.java @@ -0,0 +1,10 @@ +package io.split.android.client; + +import java.util.Set; + +interface FeatureFlagFilter { + + boolean intersect(Set values); + + boolean intersect(String values); +} diff --git a/src/main/java/io/split/android/client/FilterBuilder.java b/src/main/java/io/split/android/client/FilterBuilder.java index d794ae7e1..e562ef75c 100644 --- a/src/main/java/io/split/android/client/FilterBuilder.java +++ b/src/main/java/io/split/android/client/FilterBuilder.java @@ -1,47 +1,44 @@ package io.split.android.client; +import static com.google.common.base.Preconditions.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.SortedSet; +import java.util.TreeMap; import java.util.TreeSet; import io.split.android.client.utils.logger.Logger; -import io.split.android.client.utils.StringHelper; public class FilterBuilder { - private final static int MAX_BY_NAME_VALUES = 400; - private final static int MAX_BY_PREFIX_VALUES = 50; - private final List mFilters = new ArrayList<>(); - private final FilterGrouper mFilterGrouper = new FilterGrouper(); + private final FilterGrouper mFilterGrouper; - static private class SplitFilterComparator implements Comparator { - @Override - public int compare(SplitFilter o1, SplitFilter o2) { - return o1.getType().compareTo(o2.getType()); - } + public FilterBuilder(List filters) { + this(new FilterGrouper(), filters); } - public FilterBuilder addFilters(List filters) { - mFilters.addAll(filters); - return this; + FilterBuilder(@NonNull FilterGrouper filterGrouper, @Nullable List filters) { + mFilterGrouper = checkNotNull(filterGrouper); + addFilters(filters); } - public String build() { - - if (mFilters.size() == 0) { + public String buildQueryString() { + if (mFilters.isEmpty()) { return ""; } - StringHelper stringHelper = new StringHelper(); - StringBuilder queryString = new StringBuilder(""); - List sortedFilters = new ArrayList(mFilterGrouper.group(mFilters)); - Collections.sort(sortedFilters, new SplitFilterComparator()); + StringBuilder queryString = new StringBuilder(); + + Map sortedFilters = getGroupedFilter(); - for (SplitFilter splitFilter : sortedFilters) { + for (SplitFilter splitFilter : sortedFilters.values()) { SplitFilter.Type filterType = splitFilter.getType(); SortedSet deduptedValues = new TreeSet<>(splitFilter.getValues()); if (deduptedValues.size() < splitFilter.getValues().size()) { @@ -56,17 +53,61 @@ public String build() { queryString.append("&"); queryString.append(filterType.queryStringField()); queryString.append("="); - queryString.append(stringHelper.join(",", deduptedValues)); + queryString.append(String.join(",", deduptedValues)); } + return queryString.toString(); } + @NonNull + public Map getGroupedFilter() { + TreeMap sortedFilters = new TreeMap<>(new SplitFilterTypeComparator()); + sortedFilters.putAll(mFilterGrouper.group(mFilters)); + + return sortedFilters; + } + + private void addFilters(List filters) { + if (filters == null) { + return; + } + + boolean containsSetsFilter = false; + for (SplitFilter filter : filters) { + if (filter == null) { + continue; + } + + if (filter.getType() == SplitFilter.Type.BY_SET) { + // BY_SET filter has precedence over other filters, so we remove all other filters + // and only add BY_SET filters + Logger.w("SDK Config: The Set filter is exclusive and cannot be used simultaneously with names or prefix filters. Ignoring names and prefixes"); + if (!containsSetsFilter) { + mFilters.clear(); + containsSetsFilter = true; + } + mFilters.add(filter); + } + + if (!containsSetsFilter) { + mFilters.add(filter); + } + } + } + private void validateFilterSize(SplitFilter.Type type, int size) { if (size > type.maxValuesCount()) { - String message = "Error: " + type.maxValuesCount() + " different split " + type.queryStringField() + + String message = "Error: " + type.maxValuesCount() + " different feature flag " + type.queryStringField() + " can be specified at most. You passed " + size - + ". Please consider reducing the amount or using prefixes to target specific groups of splits."; + + ". Please consider reducing the amount or using prefixes to target specific groups of feature flags."; throw new IllegalArgumentException(message); } } + + private static class SplitFilterTypeComparator implements Comparator { + @Override + public int compare(SplitFilter.Type o1, SplitFilter.Type o2) { + return o1.compareTo(o2); + } + } } diff --git a/src/main/java/io/split/android/client/FilterGrouper.java b/src/main/java/io/split/android/client/FilterGrouper.java index 139a6ff77..7b9f52746 100644 --- a/src/main/java/io/split/android/client/FilterGrouper.java +++ b/src/main/java/io/split/android/client/FilterGrouper.java @@ -5,8 +5,14 @@ import java.util.List; import java.util.Map; -public class FilterGrouper { - public List group(List filters) { +class FilterGrouper { + + /** + * Groups filters by type + * @param filters list of filters to group + * @return map of grouped filters. The key is the filter type, the value is the filter + */ + Map group(List filters) { Map> groupedValues = new HashMap<>(); for (SplitFilter filter : filters) { List groupValues = groupedValues.get(filter.getType()); @@ -17,12 +23,13 @@ public List group(List filters) { groupValues.addAll(filter.getValues()); } - List groupedFilters = new ArrayList<>(); + Map groupedFilters = new HashMap<>(); for (Map.Entry> filterEntry : groupedValues.entrySet()) { if (filterEntry.getValue().size() > 0) { - groupedFilters.add(new SplitFilter(filterEntry.getKey(), filterEntry.getValue())); + groupedFilters.put(filterEntry.getKey(), new SplitFilter(filterEntry.getKey(), filterEntry.getValue())); } } + return groupedFilters; } } diff --git a/src/main/java/io/split/android/client/FlagSetsFilter.java b/src/main/java/io/split/android/client/FlagSetsFilter.java new file mode 100644 index 000000000..4c982e365 --- /dev/null +++ b/src/main/java/io/split/android/client/FlagSetsFilter.java @@ -0,0 +1,6 @@ +package io.split.android.client; + +import java.util.Set; + +public interface FlagSetsFilter extends FeatureFlagFilter { +} diff --git a/src/main/java/io/split/android/client/FlagSetsFilterImpl.java b/src/main/java/io/split/android/client/FlagSetsFilterImpl.java new file mode 100644 index 000000000..68c8d5501 --- /dev/null +++ b/src/main/java/io/split/android/client/FlagSetsFilterImpl.java @@ -0,0 +1,44 @@ +package io.split.android.client; + +import com.google.common.collect.Sets; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class FlagSetsFilterImpl implements FlagSetsFilter { + + private final boolean mShouldFilter; + private final Set mFlagSets; + + public FlagSetsFilterImpl(Collection flagSets) { + mFlagSets = new HashSet<>(flagSets); + mShouldFilter = !mFlagSets.isEmpty(); + } + + @Override + public boolean intersect(Set sets) { + if (!mShouldFilter) { + return true; + } + + if (sets == null) { + return false; + } + + return !Sets.intersection(mFlagSets, sets).isEmpty(); + } + + @Override + public boolean intersect(String set) { + if (!mShouldFilter) { + return true; + } + + if (set == null) { + return false; + } + + return mFlagSets.contains(set); + } +} diff --git a/src/main/java/io/split/android/client/SplitClient.java b/src/main/java/io/split/android/client/SplitClient.java index bd2fb1473..7214ffcc6 100644 --- a/src/main/java/io/split/android/client/SplitClient.java +++ b/src/main/java/io/split/android/client/SplitClient.java @@ -1,5 +1,8 @@ package io.split.android.client; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.List; import java.util.Map; @@ -50,8 +53,8 @@ public interface SplitClient extends AttributesManager { * vs. premium plan. Another example is to show a different treatment * to users created after a certain date. * - * @param featureFlagName the feature flag we want to evaluate. MUST NOT be null. - * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @param featureFlagName the feature flag we want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. * @return the evaluated treatment, the default treatment of this feature flag, or 'control'. */ String getTreatment(String featureFlagName, Map attributes); @@ -67,10 +70,10 @@ public interface SplitClient extends AttributesManager { * vs. premium plan. Another example is to show a different treatment * to users created after a certain date. * - * @param featureFlagName the feature flag we want to evaluate. MUST NOT be null. - * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @param featureFlagName the feature flag we want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. * @return the evaluated treatment, the default treatment of this feature flag, or 'control' - * with its corresponding configurations if it has one. + * with its corresponding configurations if it has one. */ SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes); @@ -81,8 +84,8 @@ public interface SplitClient extends AttributesManager { *

* It can be used to cache treatments you know it won't change very often. * - * @param featureFlagNames the feature flags you want to evaluate. MUST NOT be null. - * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @param featureFlagNames the feature flags you want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. * @return the evaluated treatments, the default treatment of a feature, or 'control'. */ Map getTreatments(List featureFlagNames, Map attributes); @@ -95,13 +98,53 @@ public interface SplitClient extends AttributesManager { *

* It can be used to cache treatments you know it won't change very often. * - * @param featureFlagNames the feature flags you want to evaluate. MUST NOT be null. - * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @param featureFlagNames the feature flags you want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. * @return the evaluated treatments, the default treatment of a feature flag, or 'control' * with its corresponding configurations if it has one. */ Map getTreatmentsWithConfig(List featureFlagNames, Map attributes); + /** + * This method is useful when you want to determine the treatment of several feature flags + * belonging to a specific Flag Set at the same time. + * + * @param flagSet the Flag Set name that you want to evaluate. Must not be null or empty + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty + * @return a {@link Map} containing for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control' + */ + Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes); + + /** + * This method is useful when you want to determine the treatment of several feature flags + * belonging to a specific list of Flag Sets at the same time. + * + * @param flagSets the Flag Sets names that you want to evaluate. Must not be null or empty + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty + * @return a {@link Map} containing for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control' + */ + Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes); + + /** + * This method is useful when you want to determine the treatment of several feature flags + * belonging to a specific Flag Set + * + * @param flagSet the Flag Set name that you want to evaluate. Must not be null or empty + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty + * @return a {@link Map} containing for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control' + */ + Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes); + + /** + * This method is useful when you want to determine the treatment of several feature flags + * belonging to a specific list of Flag Sets + * + * @param flagSets the Flag Sets names that you want to evaluate. Must not be null or empty + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty + * @return a {@link Map} containing for each feature flag the evaluated treatment, the default treatment of this feature flag, or 'control' + */ + Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes); + /** * Destroys the background processes and clears the cache, releasing the resources used by * any instances of SplitClient or SplitManager generated by the client's parent SplitFactory @@ -115,6 +158,7 @@ public interface SplitClient extends AttributesManager { /** * Checks if cached data is ready to perform treatment evaluations + * * @return true if the sdk is ready, if false, calls to getTreatment will return control */ boolean isReady(); @@ -123,118 +167,109 @@ public interface SplitClient extends AttributesManager { /** * Enqueue a new event to be sent to Split data collection services. - * + *

* The traffic type used is the one set by trafficType() in SplitClientConfig. - * + *

* Example: - * client.track(“checkout”) + * client.track(“checkout”) * * @param eventType the type of the event - * * @return true if the track was successful, false otherwise */ boolean track(String eventType); /** * Enqueue a new event to be sent to Split data collection services - * + *

* Example: - * client.track(“account”, “checkout”, 200.00) + * client.track(“account”, “checkout”, 200.00) * * @param trafficType the type of the event - * @param eventType the type of the event - * @param value the value of the event - * + * @param eventType the type of the event + * @param value the value of the event * @return true if the track was successful, false otherwise */ boolean track(String trafficType, String eventType, double value); /** * Enqueue a new event to be sent to Split data collection services - * + *

* Example: - * client.track(“account”, “checkout”) + * client.track(“account”, “checkout”) * * @param trafficType the type of the event - * @param eventType the type of the event - * + * @param eventType the type of the event * @return true if the track was successful, false otherwise */ boolean track(String trafficType, String eventType); /** * Enqueue a new event to be sent to Split data collection services - * + *

* The traffic type used is the one set by trafficType() in SplitClientConfig. - + *

* Example: - * client.track(“checkout”, 200.00) + * client.track(“checkout”, 200.00) * * @param eventType the type of the event - * @param value the value of the event - * + * @param value the value of the event * @return true if the track was successful, false otherwise */ boolean track(String eventType, double value); /** * Enqueue a new event to be sent to Split data collection services. - * + *

* The traffic type used is the one set by trafficType() in SplitClientConfig. - * + *

* Example: - * client.track(“checkout”) + * client.track(“checkout”) * - * @param eventType the type of the event + * @param eventType the type of the event * @param properties custom user data map - * * @return true if the track was successful, false otherwise */ - boolean track(String eventType, Map properties); + boolean track(String eventType, Map properties); /** * Enqueue a new event to be sent to Split data collection services - * + *

* Example: - * client.track(“account”, “checkout”, 200.00) + * client.track(“account”, “checkout”, 200.00) * * @param trafficType the type of the event - * @param eventType the type of the event - * @param value the value of the event - * @param properties custom user data map - * + * @param eventType the type of the event + * @param value the value of the event + * @param properties custom user data map * @return true if the track was successful, false otherwise */ - boolean track(String trafficType, String eventType, double value, Map properties); + boolean track(String trafficType, String eventType, double value, Map properties); /** * Enqueue a new event to be sent to split data collection services - * + *

* Example: - * client.track(“account”, “checkout”) + * client.track(“account”, “checkout”) * * @param trafficType the type of the event - * @param eventType the type of the event - * @param properties custom user data map - * + * @param eventType the type of the event + * @param properties custom user data map * @return true if the track was successful, false otherwise */ - boolean track(String trafficType, String eventType, Map properties); + boolean track(String trafficType, String eventType, Map properties); /** * Enqueue a new event to be sent to Split data collection services - * + *

* The traffic type used is the one set by trafficType() in SplitClientConfig. - + *

* Example: - * client.track(“checkout”, 200.00) + * client.track(“checkout”, 200.00) * - * @param eventType the type of the event - * @param value the value of the event + * @param eventType the type of the event + * @param value the value of the event * @param properties custom user data map - * * @return true if the track was successful, false otherwise */ - boolean track(String eventType, double value, Map properties); - + boolean track(String eventType, double value, Map properties); } diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index ea104754e..e5690d3f8 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -3,6 +3,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManagerFactory; @@ -19,6 +22,7 @@ import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.attributes.AttributesStorage; import io.split.android.client.storage.attributes.PersistentAttributesStorage; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.storage.TelemetryInitProducer; import io.split.android.client.validators.AttributesValidatorImpl; @@ -55,7 +59,8 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull ValidationMessageLogger validationLogger, @NonNull KeyValidator keyValidator, @NonNull EventsTracker eventsTracker, - @NonNull ImpressionListener customerImpressionListener) { + @NonNull ImpressionListener customerImpressionListener, + @Nullable FlagSetsFilter flagSetsFilter) { mSplitFactory = checkNotNull(splitFactory); mClientContainer = checkNotNull(clientContainer); mConfig = checkNotNull(config); @@ -72,6 +77,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, mStorageContainer.getPersistentAttributesStorage()); mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer()); mSplitValidator = new SplitValidatorImpl(); + SplitsStorage splitsStorage = mStorageContainer.getSplitsStorage(); mTreatmentManagerFactory = new TreatmentManagerFactoryImpl( keyValidator, mSplitValidator, @@ -79,7 +85,9 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, config.labelsEnabled(), new AttributesMergerImpl(), mStorageContainer.getTelemetryStorage(), - new EvaluatorImpl(mStorageContainer.getSplitsStorage(), mSplitParser) + mSplitParser, + flagSetsFilter, + splitsStorage ); } diff --git a/src/main/java/io/split/android/client/SplitClientImpl.java b/src/main/java/io/split/android/client/SplitClientImpl.java index 00f4feaf7..9ab93ad7c 100644 --- a/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/src/main/java/io/split/android/client/SplitClientImpl.java @@ -188,6 +188,26 @@ public Map getTreatmentsWithConfig(List featureFlag } } + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, mIsClientDestroyed); + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, mIsClientDestroyed); + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, mIsClientDestroyed); + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, mIsClientDestroyed); + } + public void on(SplitEvent event, SplitEventTask task) { checkNotNull(event); checkNotNull(task); diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 91489591d..4550db1ab 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -4,11 +4,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import androidx.work.WorkManager; import java.io.File; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -158,14 +160,6 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, getTelemetryStorage(shouldRecordTelemetry, telemetryStorage)); } - String buildSplitsFilterQueryString(SplitClientConfig config) { - SyncConfig syncConfig = config.syncConfig(); - if (syncConfig != null) { - return new FilterBuilder().addFilters(syncConfig.getFilters()).build(); - } - return null; - } - SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, HttpClient httpClient, String splitsFilterQueryString) throws URISyntaxException { @@ -192,9 +186,12 @@ SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, } WorkManagerWrapper buildWorkManagerWrapper(Context context, SplitClientConfig splitClientConfig, - String apiKey, String databaseName) { + String apiKey, String databaseName, Map filters) { + SplitFilter filter = filters.get(SplitFilter.Type.BY_SET) != null ? + filters.get(SplitFilter.Type.BY_SET) : + filters.get(SplitFilter.Type.BY_NAME); return new WorkManagerWrapper( - WorkManager.getInstance(context), splitClientConfig, apiKey, databaseName); + WorkManager.getInstance(context), splitClientConfig, apiKey, databaseName, filter); } @@ -417,6 +414,28 @@ SplitUpdatesWorker getSplitUpdatesWorker(SplitClientConfig config, return null; } + Pair, String> getFilterConfiguration(SyncConfig syncConfig) { + String splitsFilterQueryString = null; + Map groupedFilters = new HashMap<>(); + + if (syncConfig != null) { + FilterBuilder filterBuilder = new FilterBuilder(syncConfig.getFilters()); + groupedFilters = filterBuilder.getGroupedFilter(); + splitsFilterQueryString = filterBuilder.buildQueryString(); + } + + return new Pair<>(groupedFilters, splitsFilterQueryString); + } + + @Nullable + FlagSetsFilter getFlagSetsFilter(Map filters) { + if (filters.get(SplitFilter.Type.BY_SET) != null) { + return new FlagSetsFilterImpl(filters.get(SplitFilter.Type.BY_SET).getValues()); + } + + return null; + } + private TelemetryStorage getTelemetryStorage(boolean shouldRecordTelemetry, TelemetryStorage telemetryStorage) { if (telemetryStorage != null) { return telemetryStorage; diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index 215e596ba..2f67145d8 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -3,10 +3,12 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.core.util.Pair; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import io.split.android.client.api.Key; import io.split.android.client.common.CompressionUtilProvider; @@ -31,7 +33,6 @@ import io.split.android.client.service.impressions.ImpressionManager; import io.split.android.client.service.impressions.StrategyImpressionManager; import io.split.android.client.service.sseclient.sseclient.StreamingComponents; -import io.split.android.client.service.synchronizer.FeatureFlagsSynchronizerImpl; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.service.synchronizer.Synchronizer; import io.split.android.client.service.synchronizer.SynchronizerImpl; @@ -170,17 +171,21 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mStorageContainer = factoryHelper.buildStorageContainer(config.userConsent(), splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage); - String splitsFilterQueryString = factoryHelper.buildSplitsFilterQueryString(config); + Pair, String> filtersConfig = factoryHelper.getFilterConfiguration(config.syncConfig()); + Map filters = filtersConfig.first; + String splitsFilterQueryStringFromConfig = filtersConfig.second; SplitApiFacade splitApiFacade = factoryHelper.buildApiFacade( - config, defaultHttpClient, splitsFilterQueryString); + config, defaultHttpClient, splitsFilterQueryStringFromConfig); + + FlagSetsFilter flagSetsFilter = factoryHelper.getFlagSetsFilter(filters); SplitTaskFactory splitTaskFactory = new SplitTaskFactoryImpl( - config, splitApiFacade, mStorageContainer, splitsFilterQueryString, mEventsManagerCoordinator, - testingConfig); + config, splitApiFacade, mStorageContainer, splitsFilterQueryStringFromConfig, mEventsManagerCoordinator, + filters, flagSetsFilter, testingConfig); cleanUpDabase(splitTaskExecutor, splitTaskFactory); - WorkManagerWrapper workManagerWrapper = factoryHelper.buildWorkManagerWrapper(context, config, apiToken, databaseName); + WorkManagerWrapper workManagerWrapper = factoryHelper.buildWorkManagerWrapper(context, config, apiToken, databaseName, filters); SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); ImpressionManager impressionManager = new StrategyImpressionManager(factoryHelper.getImpressionStrategy(splitTaskExecutor, splitTaskFactory, mStorageContainer, config)); @@ -201,7 +206,8 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { impressionManager, mStorageContainer.getEventsStorage(), mEventsManagerCoordinator, - streamingComponents.getPushManagerEventBroadcaster()); + streamingComponents.getPushManagerEventBroadcaster(), + splitsFilterQueryStringFromConfig); // Only available for integration tests if (synchronizerSpy != null) { synchronizerSpy.setSynchronizer(mSynchronizer); @@ -264,7 +270,7 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { telemetrySynchronizer, mStorageContainer, splitTaskExecutor, splitApiFacade, validationLogger, keyValidator, customerImpressionListener, streamingComponents.getPushNotificationManager(), componentsRegister, workManagerWrapper, - eventsTracker); + eventsTracker, flagSetsFilter); mDestroyer = new Runnable() { public void run() { Logger.w("Shutdown called for split"); diff --git a/src/main/java/io/split/android/client/SplitFilter.java b/src/main/java/io/split/android/client/SplitFilter.java index 7aaa4457a..b775ff4bd 100644 --- a/src/main/java/io/split/android/client/SplitFilter.java +++ b/src/main/java/io/split/android/client/SplitFilter.java @@ -3,16 +3,20 @@ import androidx.annotation.NonNull; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import io.split.android.client.validators.FlagSetsValidatorImpl; +import io.split.android.client.validators.SplitFilterValidator; + public class SplitFilter { public enum Type { // Filters here has to be defined in the order // it will be in querystring BY_NAME, - BY_PREFIX; + BY_PREFIX, + BY_SET; + @NonNull @Override public String toString() { switch (this) { @@ -20,6 +24,8 @@ public String toString() { return "by split name"; case BY_PREFIX: return "by split prefix"; + case BY_SET: + return "by flag set"; default: return "Invalid type"; } @@ -31,6 +37,8 @@ public String queryStringField() { return "names"; case BY_PREFIX: return "prefixes"; + case BY_SET: + return "sets"; default: return "unknown"; } @@ -42,6 +50,8 @@ public int maxValuesCount() { return 400; case BY_PREFIX: return 50; + case BY_SET: + return 999999; default: return 0; } @@ -50,25 +60,42 @@ public int maxValuesCount() { private final SplitFilter.Type mType; private final List mValues; + private int mInvalidValueCount; + private int mTotalValueCount; - static public SplitFilter byName(@NonNull List values) { + public static SplitFilter byName(@NonNull List values) { return new SplitFilter(Type.BY_NAME, values); } - static public SplitFilter byPrefix(@NonNull List values) { + public static SplitFilter byPrefix(@NonNull List values) { return new SplitFilter(Type.BY_PREFIX, values); } + public static SplitFilter bySet(@NonNull List values) { + if (values == null) { + values = new ArrayList<>(); + } + return new SplitFilter(Type.BY_SET, values, new FlagSetsValidatorImpl()); + } + // This constructor is not private (but default) to allow Split Sync Config builder be agnostic when creating filters - // Also is not public to force SDK users to use static functions "byName" and "byPrefix" + // Also is not public to force SDK users to use static functions "byName", "byPrefix", "bySet" SplitFilter(Type type, List values) { - if(values == null) { + if (values == null) { throw new IllegalArgumentException("Values can't be null for " + type.toString() + " filter"); } mType = type; mValues = new ArrayList<>(values); } + SplitFilter(Type type, List values, SplitFilterValidator validator) { + mType = type; + SplitFilterValidator.ValidationResult validationResult = validator.cleanup("SDK config", values); + mValues = validationResult.getValues(); + mInvalidValueCount = validationResult.getInvalidValueCount(); + mTotalValueCount = (values != null) ? values.size() - validationResult.getInvalidValueCount() : 0; + } + public Type getType() { return mType; } @@ -77,8 +104,11 @@ public List getValues() { return mValues; } - public void updateValues(List values) { - mValues.clear(); - mValues.addAll(values); + public int getInvalidValueCount() { + return mInvalidValueCount; + } + + public int getTotalValueCount() { + return mTotalValueCount; } } diff --git a/src/main/java/io/split/android/client/SplitManagerImpl.java b/src/main/java/io/split/android/client/SplitManagerImpl.java index 533142730..512d8aa7e 100644 --- a/src/main/java/io/split/android/client/SplitManagerImpl.java +++ b/src/main/java/io/split/android/client/SplitManagerImpl.java @@ -142,6 +142,8 @@ private SplitView toSplitView(ParsedSplit parsedSplit) { splitView.killed = parsedSplit.killed(); splitView.changeNumber = parsedSplit.changeNumber(); splitView.configs = parsedSplit.configurations(); + splitView.sets = new ArrayList<>(parsedSplit.sets() == null ? new HashSet<>() : parsedSplit.sets()); + splitView.defaultTreatment = parsedSplit.defaultTreatment(); Set treatments = new HashSet<>(); for (ParsedCondition condition : parsedSplit.parsedConditions()) { diff --git a/src/main/java/io/split/android/client/SyncConfig.java b/src/main/java/io/split/android/client/SyncConfig.java index c0bd4e5fe..f02b62d12 100644 --- a/src/main/java/io/split/android/client/SyncConfig.java +++ b/src/main/java/io/split/android/client/SyncConfig.java @@ -13,21 +13,35 @@ public class SyncConfig { private final List mFilters; + private final int mInvalidValueCount; + private final int mTotalValueCount; - private SyncConfig(List filters) { + private SyncConfig(List filters, int invalidValueCount, int totalValueCount) { mFilters = filters; + mInvalidValueCount = invalidValueCount; + mTotalValueCount = totalValueCount; } public List getFilters() { return mFilters; } + public int getInvalidValueCount() { + return mInvalidValueCount; + } + + public int getTotalValueCount() { + return mTotalValueCount; + } + public static Builder builder() { return new Builder(); } public static class Builder { - private List mBuilderFilters = new ArrayList<>(); + private final List mBuilderFilters = new ArrayList<>(); + private int mInvalidValueCount = 0; + private int mTotalValueCount = 0; private final SplitValidator mSplitValidator = new SplitValidatorImpl(); public SyncConfig build() { @@ -46,7 +60,7 @@ public SyncConfig build() { validatedFilters.add(new SplitFilter(filter.getType(), validatedValues)); } } - return new SyncConfig(validatedFilters); + return new SyncConfig(validatedFilters, mInvalidValueCount, mTotalValueCount); } public Builder addSplitFilter(@NonNull SplitFilter filter) { @@ -54,6 +68,8 @@ public Builder addSplitFilter(@NonNull SplitFilter filter) { throw new IllegalArgumentException("Filter can't be null"); } mBuilderFilters.add(filter); + mInvalidValueCount += filter.getInvalidValueCount(); + mTotalValueCount += filter.getTotalValueCount(); return this; } } diff --git a/src/main/java/io/split/android/client/api/SplitView.java b/src/main/java/io/split/android/client/api/SplitView.java index dd13b3463..9cbc7192e 100644 --- a/src/main/java/io/split/android/client/api/SplitView.java +++ b/src/main/java/io/split/android/client/api/SplitView.java @@ -1,5 +1,8 @@ package io.split.android.client.api; +import androidx.annotation.NonNull; + +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -7,7 +10,6 @@ /** * A view of a feature flag, meant for consumption through {@link SplitManager} interface. - * */ public class SplitView { public String name; @@ -16,4 +18,7 @@ public class SplitView { public List treatments; public long changeNumber; public Map configs; + @NonNull + public List sets = new ArrayList<>(); + public String defaultTreatment; } diff --git a/src/main/java/io/split/android/client/dtos/Split.java b/src/main/java/io/split/android/client/dtos/Split.java index d74669262..395a4c3a3 100644 --- a/src/main/java/io/split/android/client/dtos/Split.java +++ b/src/main/java/io/split/android/client/dtos/Split.java @@ -1,19 +1,52 @@ package io.split.android.client.dtos; +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + import java.util.List; import java.util.Map; +import java.util.Set; public class Split { + + @SerializedName("name") public String name; + + @SerializedName("seed") public int seed; + + @SerializedName("status") public Status status; + + @SerializedName("killed") public boolean killed; + + @SerializedName("defaultTreatment") public String defaultTreatment; + + @SerializedName("conditions") public List conditions; + + @SerializedName("trafficTypeName") public String trafficTypeName; + + @SerializedName("changeNumber") public long changeNumber; + + @SerializedName("trafficAllocation") public Integer trafficAllocation; + + @SerializedName("trafficAllocationSeed") public Integer trafficAllocationSeed; + + @SerializedName("algo") public int algo; + + @SerializedName("configurations") public Map configurations; + + @Nullable + @SerializedName("sets") + public Set sets; } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index 4023e0060..089c70075 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -6,12 +6,14 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; -import io.split.android.client.Evaluator; import io.split.android.client.EvaluatorImpl; +import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; @@ -31,6 +33,7 @@ import io.split.android.client.validators.SplitValidatorImpl; import io.split.android.client.validators.TreatmentManager; import io.split.android.client.validators.TreatmentManagerImpl; +import io.split.android.client.validators.ValidationMessageLoggerImpl; import io.split.android.engine.experiments.SplitParser; import io.split.android.grammar.Treatments; @@ -45,9 +48,9 @@ public final class LocalhostSplitClient implements SplitClient { private final WeakReference mClientContainer; private final Key mKey; private final SplitEventsManager mEventsManager; - private final Evaluator mEvaluator; private final TreatmentManager mTreatmentManager; private boolean mIsClientDestroyed = false; + private final SplitsStorage mSplitsStorage; public LocalhostSplitClient(@NonNull LocalhostSplitFactory container, @NonNull SplitClientContainer clientContainer, @@ -58,17 +61,19 @@ public LocalhostSplitClient(@NonNull LocalhostSplitFactory container, @NonNull SplitParser splitParser, @NonNull AttributesManager attributesManager, @NonNull AttributesMerger attributesMerger, - @NonNull TelemetryStorageProducer telemetryStorageProducer) { + @NonNull TelemetryStorageProducer telemetryStorageProducer, + @Nullable FlagSetsFilter flagSetsFilter) { mFactoryRef = new WeakReference<>(checkNotNull(container)); mClientContainer = new WeakReference<>(checkNotNull(clientContainer)); mKey = checkNotNull(key); mEventsManager = checkNotNull(eventsManager); - mEvaluator = new EvaluatorImpl(splitsStorage, splitParser); + mSplitsStorage = splitsStorage; mTreatmentManager = new TreatmentManagerImpl(mKey.matchingKey(), mKey.bucketingKey(), - mEvaluator, new KeyValidatorImpl(), + new EvaluatorImpl(splitsStorage, splitParser), new KeyValidatorImpl(), new SplitValidatorImpl(), getImpressionsListener(splitClientConfig), - splitClientConfig.labelsEnabled(), eventsManager, attributesManager, attributesMerger, telemetryStorageProducer); + splitClientConfig.labelsEnabled(), eventsManager, attributesManager, attributesMerger, + telemetryStorageProducer, flagSetsFilter, splitsStorage, new ValidationMessageLoggerImpl()); } @Override @@ -138,6 +143,50 @@ public Map getTreatmentsWithConfig(List featureFlag } } + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + try { + return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, mIsClientDestroyed); + } catch (Exception exception) { + Logger.e(exception); + + return buildExceptionResult(Collections.singletonList(flagSet)); + } + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + try { + return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, mIsClientDestroyed); + } catch (Exception exception) { + Logger.e(exception); + + return buildExceptionResult(flagSets); + } + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + try { + return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, mIsClientDestroyed); + } catch (Exception exception) { + Logger.e(exception); + + return buildExceptionResultWithConfig(Collections.singletonList(flagSet)); + } + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + try { + return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, mIsClientDestroyed); + } catch (Exception exception) { + Logger.e(exception); + + return buildExceptionResultWithConfig(flagSets); + } + } + @Override public void destroy() { mIsClientDestroyed = true; @@ -252,4 +301,24 @@ public boolean removeAttribute(String attributeName) { public boolean clearAttributes() { return true; } + + private Map buildExceptionResult(List flagSets) { + Map result = new HashMap<>(); + Set namesByFlagSets = mSplitsStorage.getNamesByFlagSets(flagSets); + for (String featureFlagName : namesByFlagSets) { + result.put(featureFlagName, Treatments.CONTROL); + } + + return result; + } + + private Map buildExceptionResultWithConfig(List flagSets) { + Map result = new HashMap<>(); + Set namesByFlagSets = mSplitsStorage.getNamesByFlagSets(flagSets); + for (String featureFlagName : namesByFlagSets) { + result.put(featureFlagName, new SplitResult(Treatments.CONTROL)); + } + + return result; + } } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java index 364014ae7..e65838367 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java @@ -6,12 +6,22 @@ import androidx.annotation.VisibleForTesting; import java.io.IOException; - +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.FilterBuilder; +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.FlagSetsFilterImpl; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; import io.split.android.client.SplitManager; import io.split.android.client.SplitManagerImpl; +import io.split.android.client.SyncConfig; import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManagerFactory; import io.split.android.client.attributes.AttributesManagerFactoryImpl; @@ -67,6 +77,19 @@ public LocalhostSplitFactory(String key, Context context, mManager = new SplitManagerImpl(splitsStorage, new SplitValidatorImpl(), splitParser); + FlagSetsFilter flagSetsFilter = null; + if (config.syncConfig() != null) { + Map groupedFilters = new FilterBuilder(config.syncConfig().getFilters()) + .getGroupedFilter(); + + if (!groupedFilters.isEmpty()) { + SplitFilter bySetFilter = groupedFilters.get(SplitFilter.Type.BY_SET); + if (bySetFilter != null) { + flagSetsFilter = new FlagSetsFilterImpl(bySetFilter.getValues()); + } + } + } + mClientContainer = new LocalhostSplitClientContainerImpl(this, config, splitsStorage, @@ -75,9 +98,10 @@ public LocalhostSplitFactory(String key, Context context, new AttributesMergerImpl(), new NoOpTelemetryStorage(), eventsManagerCoordinator, - taskExecutor); + taskExecutor, + flagSetsFilter); - mSynchronizer = new LocalhostSynchronizer(taskExecutor, config, splitsStorage); + mSynchronizer = new LocalhostSynchronizer(taskExecutor, config, splitsStorage, buildQueryString(config.syncConfig())); mSynchronizer.start(); Logger.i("Android SDK initialized!"); @@ -141,4 +165,13 @@ public void setUserConsent(boolean enabled) { public UserConsent getUserConsent() { return UserConsent.GRANTED; } + + private static String buildQueryString(SyncConfig syncConfig) { + if (syncConfig != null) { + FilterBuilder filterBuilder = new FilterBuilder(syncConfig.getFilters()); + return filterBuilder.buildQueryString(); + } + + return ""; + } } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java index 1771ca6b3..989d9a16c 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java @@ -9,9 +9,12 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; import io.split.android.client.events.EventsManagerCoordinator; @@ -28,6 +31,7 @@ public class LocalhostSplitsStorage implements SplitsStorage { private String mLocalhostFileName; private final Context mContext; private final Map mInMemorySplits = Maps.newConcurrentMap(); + private final Map> mFlagSets = Maps.newConcurrentMap(); private final FileStorage mFileStorage; private LocalhostFileParser mParser; private final EventsManagerCoordinator mEventsManager; @@ -121,6 +125,23 @@ public void clear() { mInMemorySplits.clear(); } + @NonNull + @Override + public Set getNamesByFlagSets(Collection sets) { + Set namesToReturn = new HashSet<>(); + if (sets == null || sets.isEmpty()) { + return namesToReturn; + } + + for (String set : sets) { + Set splits = mFlagSets.get(set); + if (splits != null) { + namesToReturn.addAll(splits); + } + } + return namesToReturn; + } + private void setup() { String fileName = mLocalhostFileName; @@ -164,6 +185,20 @@ private void loadSplits() { Map values = mParser.parse(content); if (values != null) { mInMemorySplits.putAll(values); + + for (Split split : values.values()) { + Set sets = split.sets; + if (sets != null) { + for (String set : sets) { + Set splitsForSet = mFlagSets.get(set); + if (splitsForSet == null) { + splitsForSet = new HashSet<>(); + mFlagSets.put(set, splitsForSet); + } + splitsForSet.add(split.name); + } + } + } } if (!content.equals(mLastContentLoaded)) { mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java b/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java index 884c02540..6d8d73570 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSynchronizer.java @@ -3,6 +3,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import javax.security.auth.Destroyable; @@ -18,17 +19,20 @@ public class LocalhostSynchronizer implements SplitLifecycleAware, Destroyable { private final SplitTaskExecutor mTaskExecutor; private final int mRefreshRate; private final SplitsStorage mSplitsStorage; + private final String mSplitsFilterQueryStringFromConfig; public LocalhostSynchronizer(@NonNull SplitTaskExecutor taskExecutor, @NonNull SplitClientConfig splitClientConfig, - @NonNull SplitsStorage splitsStorage) { + @NonNull SplitsStorage splitsStorage, + @Nullable String splitsFilterQueryStringFromConfig) { mTaskExecutor = checkNotNull(taskExecutor); mRefreshRate = checkNotNull(splitClientConfig).offlineRefreshRate(); mSplitsStorage = checkNotNull(splitsStorage); + mSplitsFilterQueryStringFromConfig = splitsFilterQueryStringFromConfig; } public void start() { - SplitTask loadTask = new LoadSplitsTask(mSplitsStorage); + SplitTask loadTask = new LoadSplitsTask(mSplitsStorage, mSplitsFilterQueryStringFromConfig); if (mRefreshRate > 0) { mTaskExecutor.schedule( loadTask, 0, diff --git a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java index ba9775e37..f0eb208e5 100644 --- a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java @@ -1,5 +1,8 @@ package io.split.android.client.localhost.shared; +import java.util.Set; + +import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.api.Key; @@ -29,6 +32,7 @@ public class LocalhostSplitClientContainerImpl extends BaseSplitClientContainer private final TelemetryStorageProducer mTelemetryStorageProducer; private final EventsManagerCoordinator mEventsManagerCoordinator; private final SplitTaskExecutor mSplitTaskExecutor; + private final FlagSetsFilter mFlagSetsFilter; public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, SplitClientConfig config, @@ -38,7 +42,8 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, AttributesMerger attributesMerger, TelemetryStorageProducer telemetryStorageProducer, EventsManagerCoordinator eventsManagerCoordinator, - SplitTaskExecutor taskExecutor) { + SplitTaskExecutor taskExecutor, + FlagSetsFilter flagSetsFilter) { mSplitFactory = splitFactory; mConfig = config; mSplitStorage = splitsStorage; @@ -48,6 +53,7 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, mTelemetryStorageProducer = telemetryStorageProducer; mEventsManagerCoordinator = eventsManagerCoordinator; mSplitTaskExecutor = taskExecutor; + mFlagSetsFilter = flagSetsFilter; } @Override @@ -70,7 +76,8 @@ protected void createNewClient(Key key) { mSplitParser, attributesManager, mAttributesMerger, - mTelemetryStorageProducer + mTelemetryStorageProducer, + mFlagSetsFilter ); eventsManager.getExecutorResources().setSplitClient(client); diff --git a/src/main/java/io/split/android/client/service/ServiceConstants.java b/src/main/java/io/split/android/client/service/ServiceConstants.java index 3c25221c9..1ae1ffc41 100644 --- a/src/main/java/io/split/android/client/service/ServiceConstants.java +++ b/src/main/java/io/split/android/client/service/ServiceConstants.java @@ -29,6 +29,8 @@ public class ServiceConstants { public static final String WORKER_PARAM_UNIQUE_KEYS_PER_PUSH = "unique_keys_per_push"; public static final String WORKER_PARAM_UNIQUE_KEYS_ESTIMATED_SIZE_IN_BYTES = "unique_keys_estimated_size_in_bytes"; public static final String WORKER_PARAM_ENCRYPTION_ENABLED = "encryptionEnabled"; + public static final String WORKER_PARAM_CONFIGURED_FILTER_VALUES = "configuredFilterValues"; + public static final String WORKER_PARAM_CONFIGURED_FILTER_TYPE = "configuredFilterType"; public static final long LAST_SEEN_IMPRESSION_CACHE_SIZE = 500; public static final int MY_SEGMENT_V2_DATA_SIZE = 1024 * 10;// bytes diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java b/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java index 0a314fb6c..bf5beca60 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskExecutionInfo.java @@ -13,6 +13,7 @@ public class SplitTaskExecutionInfo { public static final String NON_SENT_RECORDS = "NON_SENT_RECORDS"; public static final String NON_SENT_BYTES = "NON_SENT_BYTES"; + public static final String DO_NOT_RETRY = "DO_NOT_RETRY"; final private SplitTaskType taskType; final private SplitTaskExecutionStatus status; diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java index e23407f55..defc9109f 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java @@ -18,7 +18,7 @@ public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskF SplitsSyncTask createSplitsSyncTask(boolean checkCacheExpiration); - LoadSplitsTask createLoadSplitsTask(); + LoadSplitsTask createLoadSplitsTask(String splitsFilterQueryStringFromConfig); SplitKillTask createSplitKillTask(Split split); diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java index 0bee62e7c..be036dd40 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java @@ -7,11 +7,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; -import io.split.android.client.FilterGrouper; +import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; import io.split.android.client.TestingConfig; @@ -53,11 +54,12 @@ public class SplitTaskFactoryImpl implements SplitTaskFactory { private final SplitStorageContainer mSplitsStorageContainer; private final SplitClientConfig mSplitClientConfig; private final SplitsSyncHelper mSplitsSyncHelper; - private final String mSplitsFilterQueryString; + private final String mSplitsFilterQueryStringFromConfig; private final ISplitEventsManager mEventsManager; private final TelemetryTaskFactory mTelemetryTaskFactory; private final SplitChangeProcessor mSplitChangeProcessor; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private final List mFilters; @SuppressLint("VisibleForTests") public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, @@ -65,14 +67,16 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitStorageContainer splitStorageContainer, @Nullable String splitsFilterQueryString, ISplitEventsManager eventsManager, + @Nullable Map filters, + @Nullable FlagSetsFilter flagSetsFilter, @Nullable TestingConfig testingConfig) { mSplitClientConfig = checkNotNull(splitClientConfig); mSplitApiFacade = checkNotNull(splitApiFacade); mSplitsStorageContainer = checkNotNull(splitStorageContainer); - mSplitsFilterQueryString = splitsFilterQueryString; + mSplitsFilterQueryStringFromConfig = splitsFilterQueryString; mEventsManager = eventsManager; - mSplitChangeProcessor = new SplitChangeProcessor(); + mSplitChangeProcessor = new SplitChangeProcessor(filters, flagSetsFilter); TelemetryStorage telemetryStorage = mSplitsStorageContainer.getTelemetryStorage(); mTelemetryRuntimeProducer = telemetryStorage; @@ -85,16 +89,12 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, } else { mSplitsSyncHelper = new SplitsSyncHelper(mSplitApiFacade.getSplitFetcher(), mSplitsStorageContainer.getSplitsStorage(), - new SplitChangeProcessor(), + mSplitChangeProcessor, mTelemetryRuntimeProducer); } - mTelemetryTaskFactory = new TelemetryTaskFactoryImpl(mSplitApiFacade.getTelemetryConfigRecorder(), - mSplitApiFacade.getTelemetryStatsRecorder(), - telemetryStorage, - splitClientConfig, - mSplitsStorageContainer.getSplitsStorage(), - mSplitsStorageContainer.getMySegmentsStorageContainer()); + mFilters = (filters == null) ? new ArrayList<>() : new ArrayList<>(filters.values()); + mTelemetryTaskFactory = initializeTelemetryTaskFactory(splitClientConfig, filters, telemetryStorage); } @Override @@ -115,18 +115,18 @@ public ImpressionsRecorderTask createImpressionsRecorderTask() { mSplitClientConfig.impressionsPerPush(), ServiceConstants.ESTIMATED_IMPRESSION_SIZE_IN_BYTES, mSplitClientConfig.shouldRecordTelemetry()), - mSplitsStorageContainer.getTelemetryStorage()); + mSplitsStorageContainer.getTelemetryStorage()); } @Override public SplitsSyncTask createSplitsSyncTask(boolean checkCacheExpiration) { return SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), checkCacheExpiration, - mSplitClientConfig.cacheExpirationInSeconds(), mSplitsFilterQueryString, mEventsManager, mSplitsStorageContainer.getTelemetryStorage()); + mSplitClientConfig.cacheExpirationInSeconds(), mSplitsFilterQueryStringFromConfig, mEventsManager, mSplitsStorageContainer.getTelemetryStorage()); } @Override - public LoadSplitsTask createLoadSplitsTask() { - return new LoadSplitsTask(mSplitsStorageContainer.getSplitsStorage()); + public LoadSplitsTask createLoadSplitsTask(String splitsFilterQueryStringFromConfig) { + return new LoadSplitsTask(mSplitsStorageContainer.getSplitsStorage(), splitsFilterQueryStringFromConfig); } @Override @@ -141,9 +141,8 @@ public SplitsUpdateTask createSplitsUpdateTask(long since) { @Override public FilterSplitsInCacheTask createFilterSplitsInCacheTask() { - List filters = new FilterGrouper().group(mSplitClientConfig.syncConfig().getFilters()); return new FilterSplitsInCacheTask(mSplitsStorageContainer.getPersistentSplitsStorage(), - filters, mSplitsFilterQueryString); + mFilters, mSplitsFilterQueryStringFromConfig); } @Override @@ -197,4 +196,30 @@ public TelemetryStatsRecorderTask getTelemetryStatsRecorderTask() { public SplitInPlaceUpdateTask createSplitsUpdateTask(Split featureFlag, long since) { return new SplitInPlaceUpdateTask(mSplitsStorageContainer.getSplitsStorage(), mSplitChangeProcessor, mEventsManager, mTelemetryRuntimeProducer, featureFlag, since); } + + @NonNull + private TelemetryTaskFactory initializeTelemetryTaskFactory(@NonNull SplitClientConfig splitClientConfig, @Nullable Map filters, TelemetryStorage telemetryStorage) { + final TelemetryTaskFactory mTelemetryTaskFactory; + int invalidFlagSetCount = 0; + int totalFlagSetCount = 0; + if (filters != null && !filters.isEmpty()) { + SplitFilter bySetFilter = filters.get(SplitFilter.Type.BY_SET); + if (bySetFilter != null) { + if (splitClientConfig.syncConfig() != null) { + invalidFlagSetCount = splitClientConfig.syncConfig().getInvalidValueCount(); + totalFlagSetCount = splitClientConfig.syncConfig().getTotalValueCount(); + } + } + } + + mTelemetryTaskFactory = new TelemetryTaskFactoryImpl(mSplitApiFacade.getTelemetryConfigRecorder(), + mSplitApiFacade.getTelemetryStatsRecorder(), + telemetryStorage, + splitClientConfig, + mSplitsStorageContainer.getSplitsStorage(), + mSplitsStorageContainer.getMySegmentsStorageContainer(), + totalFlagSetCount, + invalidFlagSetCount); + return mTelemetryTaskFactory; + } } diff --git a/src/main/java/io/split/android/client/service/http/HttpGeneralException.java b/src/main/java/io/split/android/client/service/http/HttpGeneralException.java index 57ef473cf..ac011d87c 100644 --- a/src/main/java/io/split/android/client/service/http/HttpGeneralException.java +++ b/src/main/java/io/split/android/client/service/http/HttpGeneralException.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; public abstract class HttpGeneralException extends Exception { + private final Integer mHttpStatus; public HttpGeneralException(String path, String message, @Nullable Integer httpStatus) { diff --git a/src/main/java/io/split/android/client/service/http/HttpStatus.java b/src/main/java/io/split/android/client/service/http/HttpStatus.java new file mode 100644 index 000000000..e78873e55 --- /dev/null +++ b/src/main/java/io/split/android/client/service/http/HttpStatus.java @@ -0,0 +1,38 @@ +package io.split.android.client.service.http; + +import androidx.annotation.Nullable; + +public enum HttpStatus { + + URI_TOO_LONG(414, "URI Too Long"); + + private final int mCode; + private final String mDescription; + + HttpStatus(int code, String description) { + mCode = code; + mDescription = description; + } + + public int getCode() { + return mCode; + } + + public String getDescription() { + return mDescription; + } + + @Nullable + public static HttpStatus fromCode(Integer code) { + if (code == null) { + return null; + } + + for (HttpStatus status : values()) { + if (status.getCode() == code) { + return status; + } + } + return null; + } +} diff --git a/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java b/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java new file mode 100644 index 000000000..8628203a2 --- /dev/null +++ b/src/main/java/io/split/android/client/service/splits/FeatureFlagProcessStrategy.java @@ -0,0 +1,71 @@ +package io.split.android.client.service.splits; + +import androidx.annotation.NonNull; + +import java.util.List; + +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.Status; + +interface FeatureFlagProcessStrategy { + + void process(List activeFeatureFlags, List archivedFeatureFlags, Split featureFlag); +} + +class StatusProcessStrategy implements FeatureFlagProcessStrategy { + + @Override + public void process(List activeFeatureFlags, List archivedFeatureFlags, Split featureFlag) { + if (featureFlag.status == Status.ACTIVE) { + activeFeatureFlags.add(featureFlag); + } else { + archivedFeatureFlags.add(featureFlag); + } + } +} + +class NamesProcessStrategy implements FeatureFlagProcessStrategy { + + private final List mConfiguredValues; + private final StatusProcessStrategy mStatusProcessStrategy; + + NamesProcessStrategy(@NonNull List configuredValues, @NonNull StatusProcessStrategy statusProcessStrategy) { + mConfiguredValues = configuredValues; + mStatusProcessStrategy = statusProcessStrategy; + } + + @Override + public void process(List activeFeatureFlags, List archivedFeatureFlags, Split featureFlag) { + // If the feature flag name is in the filter, we process it according to its status. Otherwise it is ignored + if (mConfiguredValues.contains(featureFlag.name)) { + mStatusProcessStrategy.process(activeFeatureFlags, archivedFeatureFlags, featureFlag); + } + } +} + +class SetsProcessStrategy implements FeatureFlagProcessStrategy { + + private final FlagSetsFilter mFlagSetsFilter; + private final StatusProcessStrategy mStatusProcessStrategy; + + SetsProcessStrategy(@NonNull FlagSetsFilter flagSetsFilter, @NonNull StatusProcessStrategy statusProcessStrategy) { + + mStatusProcessStrategy = statusProcessStrategy; + mFlagSetsFilter = flagSetsFilter; + } + + @Override + public void process(List activeFeatureFlags, List archivedFeatureFlags, Split featureFlag) { + if (featureFlag.sets == null || featureFlag.sets.isEmpty()) { + archivedFeatureFlags.add(featureFlag); + return; + } + + if (!mFlagSetsFilter.intersect(featureFlag.sets)) { + archivedFeatureFlags.add(featureFlag); + } else { + mStatusProcessStrategy.process(activeFeatureFlags, archivedFeatureFlags, featureFlag); + } + } +} diff --git a/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java b/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java index b81b0dbf0..98a2b02ef 100644 --- a/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java +++ b/src/main/java/io/split/android/client/service/splits/FilterSplitsInCacheTask.java @@ -23,28 +23,32 @@ public class FilterSplitsInCacheTask implements SplitTask { private final static String PREFIX_SEPARATOR = "__"; private final PersistentSplitsStorage mSplitsStorage; private final List mSplitsFilter; - private final String mSplitsFilterQueryString; + private final String mSplitsFilterQueryStringFromConfig; public FilterSplitsInCacheTask(@NonNull PersistentSplitsStorage splitsStorage, @NonNull List splitsFilter, @Nullable String splitsFilterQueryString) { mSplitsStorage = checkNotNull(splitsStorage); mSplitsFilter = checkNotNull(splitsFilter); - mSplitsFilterQueryString = splitsFilterQueryString; + mSplitsFilterQueryStringFromConfig = splitsFilterQueryString; } @Override @NonNull public SplitTaskExecutionInfo execute() { - if(!queryStringHasChanged()) { + if (!queryStringHasChanged()) { return SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE); } Set namesToKeep = new HashSet<>(); Set prefixesToKeep = new HashSet<>(); + Set setsToKeep = new HashSet<>(); for (SplitFilter filter : mSplitsFilter) { switch (filter.getType()) { + case BY_SET: + setsToKeep.addAll(filter.getValues()); + break; case BY_NAME: namesToKeep.addAll(filter.getValues()); break; @@ -52,7 +56,7 @@ public SplitTaskExecutionInfo execute() { prefixesToKeep.addAll(filter.getValues()); break; default: - Logger.e("Unknown filter type" + filter.getType().toString()); + Logger.e("Unknown filter type: " + filter.getType().toString()); } } @@ -60,14 +64,38 @@ public SplitTaskExecutionInfo execute() { List splitsInCache = mSplitsStorage.getAll(); for (Split split : splitsInCache) { String splitName = split.name; + + // Since sets filter takes precedence, + // if setsToKeep is not empty, we only keep splits that belong to the sets in setsToKeep + if (!setsToKeep.isEmpty()) { + boolean keepSplit = false; + if (split.sets != null) { + for (String set : split.sets) { + if (setsToKeep.contains(set)) { + keepSplit = true; + break; + } + } + } + + if (!keepSplit) { + splitsToDelete.add(splitName); + } + + continue; + } + + // legacy behaviour for names and prefix filters String splitPrefix = getPrefix(splitName); if (!namesToKeep.contains(split.name) && (splitPrefix == null || !prefixesToKeep.contains(splitPrefix))) { splitsToDelete.add(splitName); } } - if (splitsToDelete.size() > 0) { + + if (!splitsToDelete.isEmpty()) { mSplitsStorage.delete(splitsToDelete); } + return SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE); } @@ -80,7 +108,7 @@ private String getPrefix(String splitName) { } private boolean queryStringHasChanged() { - return !sanitizeString(mSplitsStorage.getFilterQueryString()).equals(sanitizeString(mSplitsFilterQueryString)); + return !sanitizeString(mSplitsStorage.getFilterQueryString()).equals(sanitizeString(mSplitsFilterQueryStringFromConfig)); } private String sanitizeString(String string) { diff --git a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java index be0015060..1c093a803 100644 --- a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java +++ b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java @@ -12,18 +12,31 @@ public class LoadSplitsTask implements SplitTask { private final SplitsStorage mSplitsStorage; + private final String mSplitsFilterQueryStringFromConfig; - public LoadSplitsTask(SplitsStorage splitsStorage) { + public LoadSplitsTask(SplitsStorage splitsStorage, String splitsFilterQueryStringFromConfig) { mSplitsStorage = checkNotNull(splitsStorage); + mSplitsFilterQueryStringFromConfig = (splitsFilterQueryStringFromConfig == null) ? "" : splitsFilterQueryStringFromConfig; } @Override @NonNull public SplitTaskExecutionInfo execute() { mSplitsStorage.loadLocal(); - if(mSplitsStorage.getTill() > -1) { + String queryStringFromStorage = mSplitsStorage.getSplitsFilterQueryString(); + if (queryStringFromStorage == null) { + queryStringFromStorage = ""; + } + + if (mSplitsStorage.getTill() > -1 && mSplitsFilterQueryStringFromConfig.equals(queryStringFromStorage)) { return SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS); } + + if (!mSplitsFilterQueryStringFromConfig.equals(queryStringFromStorage)) { + mSplitsStorage.clear(); + mSplitsStorage.updateSplitsFilterQueryString(mSplitsFilterQueryStringFromConfig); + } + return SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_SPLITS); } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java b/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java index 38e643af9..3c0534bd7 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java +++ b/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java @@ -1,17 +1,53 @@ package io.split.android.client.service.splits; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.SplitFilter; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; -import io.split.android.client.dtos.Status; import io.split.android.client.storage.splits.ProcessedSplitChange; public class SplitChangeProcessor { + + private final SplitFilter mSplitFilter; + + private final StatusProcessStrategy mStatusProcessStrategy; + + private final FlagSetsFilter mFlagSetsFilter; + + /** @noinspection unused*/ // Used in tests + private SplitChangeProcessor() { + mSplitFilter = null; + mStatusProcessStrategy = new StatusProcessStrategy(); + mFlagSetsFilter = null; + } + + public SplitChangeProcessor(@Nullable Map filters, FlagSetsFilter flagSetsFilter) { + // We're only supporting one filter type + if (filters == null || filters.isEmpty()) { + mSplitFilter = null; + } else { + mSplitFilter = filters.values().iterator().next(); + } + + mStatusProcessStrategy = new StatusProcessStrategy(); + mFlagSetsFilter = flagSetsFilter; + } + + public SplitChangeProcessor(@Nullable SplitFilter splitFilter, @Nullable FlagSetsFilter flagSetsFilter) { + mSplitFilter = splitFilter; + mFlagSetsFilter = flagSetsFilter; + mStatusProcessStrategy = new StatusProcessStrategy(); + } + public ProcessedSplitChange process(SplitChange splitChange) { if (splitChange == null || splitChange.splits == null) { return new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), -1L, 0); @@ -20,25 +56,39 @@ public ProcessedSplitChange process(SplitChange splitChange) { return buildProcessedSplitChange(splitChange.splits, splitChange.till); } - public ProcessedSplitChange process(Split split, long changeNumber) { - return buildProcessedSplitChange(Collections.singletonList(split), changeNumber); + public ProcessedSplitChange process(Split featureFlag, long changeNumber) { + return buildProcessedSplitChange(Collections.singletonList(featureFlag), changeNumber); } @NonNull - private static ProcessedSplitChange buildProcessedSplitChange(List featureFlags, long changeNumber) { - List activeSplits = new ArrayList<>(); - List archivedSplits = new ArrayList<>(); - for (Split split : featureFlags) { - if (split.name == null) { + private ProcessedSplitChange buildProcessedSplitChange(List featureFlags, long changeNumber) { + List activeFeatureFlags = new ArrayList<>(); + List archivedFeatureFlags = new ArrayList<>(); + + FeatureFlagProcessStrategy processStrategy = getProcessStrategy(mSplitFilter); + + for (Split featureFlag : featureFlags) { + if (featureFlag == null || featureFlag.name == null) { continue; } - if (split.status == Status.ACTIVE) { - activeSplits.add(split); - } else { - archivedSplits.add(split); - } + + processStrategy.process(activeFeatureFlags, archivedFeatureFlags, featureFlag); } - return new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, System.currentTimeMillis() / 100); + return new ProcessedSplitChange(activeFeatureFlags, archivedFeatureFlags, changeNumber, System.currentTimeMillis() / 100); + } + + private FeatureFlagProcessStrategy getProcessStrategy(SplitFilter splitFilter) { + if (splitFilter == null || splitFilter.getValues().isEmpty()) { + return mStatusProcessStrategy; + } + + if (splitFilter.getType() == SplitFilter.Type.BY_SET && mFlagSetsFilter != null) { + return new SetsProcessStrategy(mFlagSetsFilter, mStatusProcessStrategy); + } else if (splitFilter.getType() == SplitFilter.Type.BY_NAME) { + return new NamesProcessStrategy(splitFilter.getValues(), mStatusProcessStrategy); + } else { + return mStatusProcessStrategy; + } } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java index e9d072cf6..c34dd6539 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -49,7 +49,7 @@ public SplitTaskExecutionInfo execute() { mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); - Logger.d("Updated feature flag: " + mSplit.name); + Logger.v("Updated feature flag"); return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } catch (Exception ex) { Logger.e("Could not update feature flag"); diff --git a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java b/src/main/java/io/split/android/client/service/splits/SplitKillTask.java index bdf91da04..06b8be469 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitKillTask.java @@ -31,18 +31,23 @@ public SplitKillTask(@NonNull SplitsStorage splitsStorage, Split split, public SplitTaskExecutionInfo execute() { try { if (mKilledSplit == null) { - logError("Split name to kill could not be null."); + logError("Feature flag name to kill could not be null."); return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); } long changeNumber = mSplitsStorage.getTill(); if (mKilledSplit.changeNumber <= changeNumber) { - Logger.d("Skipping killed split notification for old change number: " + Logger.d("Skipping killed feature flag notification for old change number: " + mKilledSplit.changeNumber); return SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL); } Split splitToKill = mSplitsStorage.get(mKilledSplit.name); + if (splitToKill == null) { + Logger.d("Skipping " + mKilledSplit.name + " since not in storage"); + return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); + } + splitToKill.killed = true; splitToKill.defaultTreatment = mKilledSplit.defaultTreatment; splitToKill.changeNumber = mKilledSplit.changeNumber; @@ -50,14 +55,14 @@ public SplitTaskExecutionInfo execute() { mSplitsStorage.updateWithoutChecks(splitToKill); mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); } catch (Exception e) { - logError("Unknown error while updating killed split: " + e.getLocalizedMessage()); + logError("Unknown error while updating killed feature flag: " + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); } - Logger.d("Killed split has been updated"); + Logger.d("Killed feature flag has been updated"); return SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL); } private void logError(String message) { - Logger.e("Error while executing Split kill task: " + message); + Logger.e("Error while executing feature flag kill task: " + message); } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index 9693f4875..8f1b8ced5 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -18,6 +19,7 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; +import io.split.android.client.service.http.HttpStatus; import io.split.android.client.service.sseclient.BackoffCounter; import io.split.android.client.service.sseclient.ReconnectBackoffCounter; import io.split.android.client.storage.splits.SplitsStorage; @@ -81,6 +83,12 @@ private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolea logError("Network error while fetching feature flags" + e.getLocalizedMessage()); mTelemetryRuntimeProducer.recordSyncError(OperationType.SPLITS, e.getHttpStatus()); + if (HttpStatus.fromCode(e.getHttpStatus()) == HttpStatus.URI_TOO_LONG) { + Logger.e("SDK initialization: the amount of flag sets provided is big, causing URI length error"); + return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC, + Collections.singletonMap(SplitTaskExecutionInfo.DO_NOT_RETRY, true)); + } + return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); } catch (Exception e) { logError("Unexpected while fetching feature flags" + e.getLocalizedMessage()); diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 9e256fd46..d3c3458de 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -17,7 +17,7 @@ public class SplitsSyncTask implements SplitTask { - private final String mSplitsFilterQueryString; + private final String mSplitsFilterQueryStringFromConfig; private final SplitsStorage mSplitsStorage; private final boolean mCheckCacheExpiration; @@ -59,7 +59,7 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, mSplitsSyncHelper = checkNotNull(splitsSyncHelper); mCacheExpirationInSeconds = cacheExpirationInSeconds; mCheckCacheExpiration = checkCacheExpiration; - mSplitsFilterQueryString = splitsFilterQueryString; + mSplitsFilterQueryStringFromConfig = splitsFilterQueryString; mEventsManager = eventsManager; mChangeChecker = new SplitsChangeChecker(); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); @@ -77,7 +77,7 @@ public SplitTaskExecutionInfo execute() { boolean splitsFilterHasChanged = splitsFilterHasChanged(mSplitsStorage.getSplitsFilterQueryString()); if (splitsFilterHasChanged) { - mSplitsStorage.updateSplitsFilterQueryString(mSplitsFilterQueryString); + mSplitsStorage.updateSplitsFilterQueryString(mSplitsFilterQueryStringFromConfig); storedChangeNumber = -1; } @@ -107,7 +107,7 @@ private void notifyInternalEvent(long storedChangeNumber) { } private boolean splitsFilterHasChanged(String storedSplitsFilterQueryString) { - return !sanitizeString(mSplitsFilterQueryString).equals(sanitizeString(storedSplitsFilterQueryString)); + return !sanitizeString(mSplitsFilterQueryStringFromConfig).equals(sanitizeString(storedSplitsFilterQueryString)); } private String sanitizeString(String string) { diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java index e579373f4..018dfedf4 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java @@ -30,7 +30,7 @@ public class RetryBackoffCounterTimer implements SplitTaskExecutionListener { /** * Creates an instance which retries tasks indefinitely, using the strategy defined by backoffCounter. * - * @param taskExecutor Implementation of SplitTaskExecutor. + * @param taskExecutor Implementation of SplitTaskExecutor. * @param backoffCounter Will determine the retry interval. */ public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, @@ -43,8 +43,8 @@ public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, /** * Creates an instance which retries tasks up to the number of times specified by retryAttemptsLimit. * - * @param taskExecutor Implementation of SplitTaskExecutor. - * @param backoffCounter Will determine the retry interval. + * @param taskExecutor Implementation of SplitTaskExecutor. + * @param backoffCounter Will determine the retry interval. * @param retryAttemptsLimit Maximum number of attempts for task retry. */ public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, @@ -65,7 +65,7 @@ synchronized public void setTask(@NonNull SplitTask task) { } synchronized public void stop() { - if(mTask == null) { + if (mTask == null) { return; } mTaskExecutor.stopTask(mTaskId); @@ -73,7 +73,7 @@ synchronized public void stop() { } synchronized public void start() { - if(mTask == null || mTaskId != null) { + if (mTask == null || mTaskId != null) { return; } mBackoffCounter.resetCounter(); @@ -94,16 +94,25 @@ synchronized private void schedule() { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mTaskId = null; - if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR && + (taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY) == null || + Boolean.FALSE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)))) { + if (mRetryAttemptsLimit == DEFAULT_MAX_ATTEMPTS || mCurrentAttempts.get() < mRetryAttemptsLimit) { schedule(); } + return; } mBackoffCounter.resetCounter(); + if (mListener != null) { - mListener.taskExecuted(SplitTaskExecutionInfo.success(taskInfo.getTaskType())); + if (taskInfo.getStatus() == SplitTaskExecutionStatus.SUCCESS) { + mListener.taskExecuted(SplitTaskExecutionInfo.success(taskInfo.getTaskType())); + } else if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + mListener.taskExecuted(SplitTaskExecutionInfo.error(taskInfo.getTaskType())); + } } } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java index 04b87e777..6b48eeccd 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java @@ -6,8 +6,6 @@ public interface FeatureFlagsSynchronizer { void loadAndSynchronize(); - void loadFromCache(); - void synchronize(long since); void synchronize(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java index 2e8a66dc1..a60f9bad1 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java @@ -37,6 +37,7 @@ public class FeatureFlagsSynchronizerImpl implements FeatureFlagsSynchronizer { private final RetryBackoffCounterTimer mSplitsUpdateRetryTimer; @Nullable private final SplitTaskExecutionListener mSplitsSyncListener; + private final String mSplitsFilterQueryStringFromConfig; public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitTaskExecutor taskExecutor, @@ -44,7 +45,8 @@ public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig @NonNull SplitTaskFactory splitTaskFactory, @NonNull ISplitEventsManager splitEventsManager, @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, - @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @NonNull String splitsFilterQueryStringFromConfig) { mTaskExecutor = checkNotNull(taskExecutor); mSplitsTaskExecutor = splitSingleThreadTaskExecutor; @@ -69,18 +71,14 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mSplitsSyncRetryTimer.setTask(mSplitTaskFactory.createSplitsSyncTask(true), mSplitsSyncListener); mLoadLocalSplitsListener = new LoadLocalDataListener( splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - } - - @Override - public void loadFromCache() { - submitLoadingTask(mLoadLocalSplitsListener); + mSplitsFilterQueryStringFromConfig = splitsFilterQueryStringFromConfig; } @Override public void loadAndSynchronize() { List enqueued = new ArrayList<>(); enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createFilterSplitsInCacheTask(), null)); - enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(), mLoadLocalSplitsListener)); + enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(mSplitsFilterQueryStringFromConfig), mLoadLocalSplitsListener)); enqueued.add(new SplitTaskBatchItem(() -> { synchronize(); return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); @@ -117,7 +115,7 @@ public void stopSynchronization() { @Override public void submitLoadingTask(SplitTaskExecutionListener listener) { - mTaskExecutor.submit(mSplitTaskFactory.createLoadSplitsTask(), + mTaskExecutor.submit(mSplitTaskFactory.createLoadSplitsTask(mSplitsFilterQueryStringFromConfig), listener); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java index 5c5992d53..6f8e614a4 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java @@ -8,8 +8,6 @@ public interface Synchronizer extends SplitLifecycleAware { void loadAndSynchronizeSplits(); - void loadSplitsFromCache(); - void loadMySegmentsFromCache(); void loadAttributesFromCache(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index ad4ad8f9a..a21eeb436 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -63,7 +63,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull ImpressionManager impressionManager, @NonNull StoragePusher eventsStorage, @NonNull ISplitEventsManager eventsManagerCoordinator, - @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @NonNull String splitsFilterQueryStringFromConfig) { this(splitClientConfig, taskExecutor, splitSingleThreadTaskExecutor, @@ -79,7 +80,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, splitTaskFactory, eventsManagerCoordinator, retryBackoffCounterTimerFactory, - pushManagerEventBroadcaster), + pushManagerEventBroadcaster, + splitsFilterQueryStringFromConfig), eventsStorage); } @@ -122,11 +124,6 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, } } - @Override - public void loadSplitsFromCache() { - mFeatureFlagsSynchronizer.loadFromCache(); - } - @Override public void loadMySegmentsFromCache() { mMySegmentsSynchronizerRegistry.loadMySegmentsFromCache(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java b/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java index e80480315..8a255706e 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java +++ b/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Observer; import androidx.lifecycle.ProcessLifecycleOwner; import androidx.work.Constraints; @@ -23,6 +22,7 @@ import java.util.concurrent.TimeUnit; import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFilter; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -45,18 +45,22 @@ public class WorkManagerWrapper implements MySegmentsWorkManagerWrapper { private WeakReference mFetcherExecutionListener; // This variable is used to avoid loading data first time // we receive enqueued event - final private Set mShouldLoadFromLocal; + private final Set mShouldLoadFromLocal; + @Nullable + private final SplitFilter mFilter; public WorkManagerWrapper(@NonNull WorkManager workManager, @NonNull SplitClientConfig splitClientConfig, @NonNull String apiKey, - @NonNull String databaseName) { + @NonNull String databaseName, + @Nullable SplitFilter filter) { mWorkManager = checkNotNull(workManager); mDatabaseName = checkNotNull(databaseName); mSplitClientConfig = checkNotNull(splitClientConfig); mApiKey = checkNotNull(apiKey); mShouldLoadFromLocal = new HashSet<>(); mConstraints = buildConstraints(); + mFilter = filter; } public void setFetcherExecutionListener(SplitTaskExecutionListener fetcherExecutionListener) { @@ -184,6 +188,8 @@ private Data buildSplitSyncInputData() { dataBuilder.putLong(ServiceConstants.WORKER_PARAM_SPLIT_CACHE_EXPIRATION, mSplitClientConfig.cacheExpirationInSeconds()); dataBuilder.putString(ServiceConstants.WORKER_PARAM_ENDPOINT, mSplitClientConfig.endpoint()); dataBuilder.putBoolean(ServiceConstants.SHOULD_RECORD_TELEMETRY, mSplitClientConfig.shouldRecordTelemetry()); + dataBuilder.putString(ServiceConstants.WORKER_PARAM_CONFIGURED_FILTER_TYPE, (mFilter != null) ? mFilter.getType().queryStringField() : null); + dataBuilder.putStringArray(ServiceConstants.WORKER_PARAM_CONFIGURED_FILTER_VALUES, (mFilter != null) ? mFilter.getValues().toArray(new String[0]) : new String[0]); return buildInputData(dataBuilder.build()); } diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java index 624b0f948..08a24f672 100644 --- a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java @@ -2,9 +2,11 @@ import androidx.annotation.NonNull; +import java.util.List; + import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFilter; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.Config; @@ -15,7 +17,6 @@ import io.split.android.client.telemetry.storage.TelemetryStatsProvider; import io.split.android.client.telemetry.storage.TelemetryStatsProviderImpl; import io.split.android.client.telemetry.storage.TelemetryStorage; -import io.split.android.client.telemetry.storage.TelemetryStorageConsumer; public class TelemetryTaskFactoryImpl implements TelemetryTaskFactory { @@ -30,9 +31,11 @@ public TelemetryTaskFactoryImpl(@NonNull HttpRecorder telemetryConfigRec @NonNull TelemetryStorage telemetryStorage, @NonNull SplitClientConfig splitClientConfig, @NonNull SplitsStorage splitsStorage, - @NonNull MySegmentsStorageContainer mySegmentsStorageContainer) { + @NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + int flagSetCount, + int invalidFlagSetCount) { mTelemetryConfigRecorder = telemetryConfigRecorder; - mTelemetryConfigProvider = new TelemetryConfigProviderImpl(telemetryStorage, splitClientConfig); + mTelemetryConfigProvider = new TelemetryConfigProviderImpl(telemetryStorage, splitClientConfig, flagSetCount, invalidFlagSetCount); mTelemetryStatsRecorder = telemetryStatsRecorder; mTelemetryStatsProvider = new TelemetryStatsProviderImpl(telemetryStorage, splitsStorage, mySegmentsStorageContainer); mTelemetryRuntimeProducer = telemetryStorage; diff --git a/src/main/java/io/split/android/client/service/workmanager/SplitsSyncWorker.java b/src/main/java/io/split/android/client/service/workmanager/SplitsSyncWorker.java index f851908a1..982e2fcae 100644 --- a/src/main/java/io/split/android/client/service/workmanager/SplitsSyncWorker.java +++ b/src/main/java/io/split/android/client/service/workmanager/SplitsSyncWorker.java @@ -3,11 +3,19 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.work.WorkerParameters; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.FlagSetsFilterImpl; +import io.split.android.client.SplitFilter; import io.split.android.client.dtos.SplitChange; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.ServiceFactory; @@ -32,6 +40,9 @@ public SplitsSyncWorker(@NonNull Context context, String apiKey = workerParams.getInputData().getString(ServiceConstants.WORKER_PARAM_API_KEY); boolean encryptionEnabled = workerParams.getInputData().getBoolean(ServiceConstants.WORKER_PARAM_ENCRYPTION_ENABLED, false); + SplitFilter filter = buildFilter(workerParams.getInputData().getString(ServiceConstants.WORKER_PARAM_CONFIGURED_FILTER_TYPE), + workerParams.getInputData().getStringArray(ServiceConstants.WORKER_PARAM_CONFIGURED_FILTER_VALUES)); + SplitsStorage splitsStorage = StorageFactory.getSplitsStorageForWorker(getDatabase(), apiKey, encryptionEnabled); // StorageFactory.getSplitsStorageForWorker creates a new storage instance, so it needs // to be populated by calling loadLocal @@ -41,7 +52,12 @@ public SplitsSyncWorker(@NonNull Context context, TelemetryStorage telemetryStorage = StorageFactory.getTelemetryStorage(shouldRecordTelemetry); - SplitsSyncHelper splitsSyncHelper = new SplitsSyncHelper(splitsFetcher, splitsStorage, new SplitChangeProcessor(), telemetryStorage); + SplitChangeProcessor splitChangeProcessor = new SplitChangeProcessor(filter, (filter != null && filter.getType() == SplitFilter.Type.BY_SET) ? + new FlagSetsFilterImpl(filter.getValues()) : null); + + SplitsSyncHelper splitsSyncHelper = new SplitsSyncHelper(splitsFetcher, splitsStorage, + splitChangeProcessor, + telemetryStorage); mSplitTask = buildSplitSyncTask(splitsStorage, telemetryStorage, splitsSyncHelper); } catch (URISyntaxException e) { @@ -49,6 +65,24 @@ public SplitsSyncWorker(@NonNull Context context, } } + @Nullable + private static SplitFilter buildFilter(String filterType, String[] filterValuesArray) { + SplitFilter filter = null; + if (filterType != null) { + List configuredFilterValues = new ArrayList<>(); + if (filterValuesArray != null) { + configuredFilterValues = Arrays.asList(filterValuesArray); + } + + if (SplitFilter.Type.BY_NAME.queryStringField().equals(filterType)) { + filter = SplitFilter.byName(configuredFilterValues); + } else if (SplitFilter.Type.BY_SET.queryStringField().equals(filterType)) { + filter = SplitFilter.bySet(configuredFilterValues); + } + } + return filter; + } + @NonNull private SplitTask buildSplitSyncTask(SplitsStorage splitsStorage, TelemetryStorage telemetryStorage, SplitsSyncHelper splitsSyncHelper) { return SplitsSyncTask.buildForBackground(splitsSyncHelper, diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 742c85c68..007e868f6 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -6,9 +6,11 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.EventsTracker; +import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitClientFactory; @@ -70,7 +72,8 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @Nullable PushNotificationManager pushNotificationManager, @NonNull ClientComponentsRegister clientComponentsRegister, @NonNull MySegmentsWorkManagerWrapper workManagerWrapper, - @NonNull EventsTracker eventsTracker) { + @NonNull EventsTracker eventsTracker, + @Nullable FlagSetsFilter flagSetsFilter) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; mStreamingEnabled = config.streamingEnabled(); @@ -88,7 +91,8 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, validationLogger, keyValidator, eventsTracker, - customerImpressionListener + customerImpressionListener, + flagSetsFilter ); mClientComponentsRegister = checkNotNull(clientComponentsRegister); mSplitTaskExecutor = checkNotNull(splitTaskExecutor); diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java index 038ad185d..62af70e7b 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java @@ -3,8 +3,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; @@ -32,4 +34,7 @@ public interface SplitsStorage { void updateSplitsFilterQueryString(String queryString); void clear(); + + @NonNull + Set getNamesByFlagSets(Collection flagSets); } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java index 5f4374bbd..b143c209c 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java @@ -6,9 +6,12 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import io.split.android.client.dtos.Split; @@ -17,6 +20,7 @@ public class SplitsStorageImpl implements SplitsStorage { private final PersistentSplitsStorage mPersistentStorage; private final Map mInMemorySplits; + private final Map> mFlagSets; private long mChangeNumber; private long mUpdateTimestamp; private String mSplitsFilterQueryString; @@ -26,6 +30,7 @@ public SplitsStorageImpl(@NonNull PersistentSplitsStorage persistentStorage) { mPersistentStorage = checkNotNull(persistentStorage); mInMemorySplits = new ConcurrentHashMap<>(); mTrafficTypes = new ConcurrentHashMap<>(); + mFlagSets = new ConcurrentHashMap<>(); } @Override @@ -38,6 +43,7 @@ public void loadLocal() { mSplitsFilterQueryString = snapshot.getSplitsFilterQueryString(); for (Split split : splits) { mInMemorySplits.put(split.name, split); + addOrUpdateFlagSets(split); increaseTrafficTypeCount(split.trafficTypeName); } } @@ -86,6 +92,7 @@ public void update(ProcessedSplitChange splitChange) { } increaseTrafficTypeCount(split.trafficTypeName); mInMemorySplits.put(split.name, split); + addOrUpdateFlagSets(split); } } @@ -93,6 +100,7 @@ public void update(ProcessedSplitChange splitChange) { for (Split split : archivedSplits) { if (mInMemorySplits.remove(split.name) != null) { decreaseTrafficTypeCount(split.trafficTypeName); + deleteFromFlagSetsIfNecessary(split); } } } @@ -107,6 +115,7 @@ public void update(ProcessedSplitChange splitChange) { public void updateWithoutChecks(Split split) { mInMemorySplits.put(split.name, split); mPersistentStorage.update(split); + deleteFromFlagSets(split); } @Override @@ -127,6 +136,7 @@ public String getSplitsFilterQueryString() { @WorkerThread public void updateSplitsFilterQueryString(String queryString) { mPersistentStorage.updateFilterQueryString(queryString); + mSplitsFilterQueryString = queryString; } @Override @@ -135,6 +145,26 @@ public void clear() { mInMemorySplits.clear(); mChangeNumber = -1; mPersistentStorage.clear(); + mFlagSets.clear(); + mTrafficTypes.clear(); + } + + @NonNull + @Override + public Set getNamesByFlagSets(Collection sets) { + Set namesToReturn = new HashSet<>(); + if (sets == null || sets.isEmpty()) { + return namesToReturn; + } + + for (String set : sets) { + Set splits = mFlagSets.get(set); + if (splits != null) { + namesToReturn.addAll(splits); + } + } + + return namesToReturn; } @Override @@ -177,4 +207,47 @@ private int countForTrafficType(@NonNull String name) { } return count; } + + private void addOrUpdateFlagSets(Split split) { + if (split.sets == null) { + return; + } + + for (String set : split.sets) { + Set splitsForSet = mFlagSets.get(set); + if (splitsForSet == null) { + splitsForSet = new HashSet<>(); + mFlagSets.put(set, splitsForSet); + } + splitsForSet.add(split.name); + } + + deleteFromFlagSetsIfNecessary(split); + } + + private void deleteFromFlagSetsIfNecessary(Split featureFlag) { + if (featureFlag.sets == null) { + return; + } + + for (String set : mFlagSets.keySet()) { + if (featureFlag.sets.contains(set)) { + continue; + } + + Set flagsForSet = mFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } + + private void deleteFromFlagSets(Split featureFlag) { + for (String set : mFlagSets.keySet()) { + Set flagsForSet = mFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } } diff --git a/src/main/java/io/split/android/client/telemetry/model/Config.java b/src/main/java/io/split/android/client/telemetry/model/Config.java index 25ec3ee42..bf6849271 100644 --- a/src/main/java/io/split/android/client/telemetry/model/Config.java +++ b/src/main/java/io/split/android/client/telemetry/model/Config.java @@ -60,6 +60,12 @@ public class Config { @SerializedName("i") private List integrations; + @SerializedName("fsT") + private int flagSetsTotal; + + @SerializedName("fsI") + private int flagSetsInvalid; + public int getOperationMode() { return operationMode; } @@ -195,4 +201,20 @@ public List getIntegrations() { public void setIntegrations(List integrations) { this.integrations = integrations; } + + public int getFlagSetsTotal() { + return flagSetsTotal; + } + + public void setFlagSetsTotal(int flagSetsTotal) { + this.flagSetsTotal = flagSetsTotal; + } + + public int getFlagSetsInvalid() { + return flagSetsInvalid; + } + + public void setFlagSetsInvalid(int flagSetsInvalid) { + this.flagSetsInvalid = flagSetsInvalid; + } } diff --git a/src/main/java/io/split/android/client/telemetry/model/Method.java b/src/main/java/io/split/android/client/telemetry/model/Method.java index da5a4d6a8..2303325d0 100644 --- a/src/main/java/io/split/android/client/telemetry/model/Method.java +++ b/src/main/java/io/split/android/client/telemetry/model/Method.java @@ -5,6 +5,10 @@ public enum Method { TREATMENTS("getTreatments"), TREATMENT_WITH_CONFIG("getTreatmentWithConfig"), TREATMENTS_WITH_CONFIG("getTreatmentsWithConfig"), + TREATMENTS_BY_FLAG_SET("getTreatmentsByFlagSet"), + TREATMENTS_BY_FLAG_SETS("getTreatmentsByFlagSets"), + TREATMENTS_WITH_CONFIG_BY_FLAG_SET("getTreatmentsWithConfigByFlagSet"), + TREATMENTS_WITH_CONFIG_BY_FLAG_SETS("getTreatmentsWithConfigByFlagSets"), TRACK("track"); private final String _method; diff --git a/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java b/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java index 2a580e2c5..0a936f565 100644 --- a/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java +++ b/src/main/java/io/split/android/client/telemetry/model/MethodExceptions.java @@ -16,6 +16,18 @@ public class MethodExceptions { @SerializedName("tcs") private long treatmentsWithConfig; + @SerializedName("tf") + private long treatmentsByFlagSet; + + @SerializedName("tfs") + private long treatmentsByFlagSets; + + @SerializedName("tcf") + private long treatmentsWithConfigByFlagSet; + + @SerializedName("tcfs") + private long treatmentsWithConfigByFlagSets; + @SerializedName("tr") private long track; @@ -51,6 +63,38 @@ public void setTreatmentsWithConfig(long treatmentsWithConfig) { this.treatmentsWithConfig = treatmentsWithConfig; } + public void setTreatmentsByFlagSet(long treatmentsByFlagSet) { + this.treatmentsByFlagSet = treatmentsByFlagSet; + } + + public long getTreatmentsByFlagSet() { + return treatmentsByFlagSet; + } + + public void setTreatmentsByFlagSets(long treatmentsByFlagSets) { + this.treatmentsByFlagSets = treatmentsByFlagSets; + } + + public long getTreatmentsByFlagSets() { + return treatmentsByFlagSets; + } + + public void setTreatmentsWithConfigByFlagSet(long treatmentsWithConfigByFlagSet) { + this.treatmentsWithConfigByFlagSet = treatmentsWithConfigByFlagSet; + } + + public long getTreatmentsWithConfigByFlagSet() { + return treatmentsWithConfigByFlagSet; + } + + public void setTreatmentsWithConfigByFlagSets(long treatmentsWithConfigByFlagSets) { + this.treatmentsWithConfigByFlagSets = treatmentsWithConfigByFlagSets; + } + + public long getTreatmentsWithConfigByFlagSets() { + return treatmentsWithConfigByFlagSets; + } + public long getTrack() { return track; } diff --git a/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java b/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java index df631118e..15cc73fe8 100644 --- a/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java +++ b/src/main/java/io/split/android/client/telemetry/model/MethodLatencies.java @@ -18,6 +18,18 @@ public class MethodLatencies { @SerializedName("tcs") private List treatmentsWithConfig; + @SerializedName("tf") + private List treatmentsByFlagSet; + + @SerializedName("tfs") + private List treatmentsByFlagSets; + + @SerializedName("tcf") + private List treatmentsWithConfigByFlagSet; + + @SerializedName("tcfs") + private List treatmentsWithConfigByFlagSets; + @SerializedName("tr") private List track; @@ -53,6 +65,38 @@ public void setTreatmentsWithConfig(List treatmentsWithConfig) { this.treatmentsWithConfig = treatmentsWithConfig; } + public void setTreatmentsByFlagSet(List treatmentsByFlagSet) { + this.treatmentsByFlagSet = treatmentsByFlagSet; + } + + public List getTreatmentsByFlagSet() { + return treatmentsByFlagSet; + } + + public void setTreatmentsByFlagSets(List treatmentsByFlagSets) { + this.treatmentsByFlagSets = treatmentsByFlagSets; + } + + public List getTreatmentsByFlagSets() { + return treatmentsByFlagSets; + } + + public void setTreatmentsWithConfigByFlagSet(List treatmentsWithConfigByFlagSet) { + this.treatmentsWithConfigByFlagSet = treatmentsWithConfigByFlagSet; + } + + public List getTreatmentsWithConfigByFlagSet() { + return treatmentsWithConfigByFlagSet; + } + + public void setTreatmentsWithConfigByFlagSets(List treatmentsWithConfigByFlagSets) { + this.treatmentsWithConfigByFlagSets = treatmentsWithConfigByFlagSets; + } + + public List getTreatmentsWithConfigByFlagSets() { + return treatmentsWithConfigByFlagSets; + } + public List getTrack() { return track; } diff --git a/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java b/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java index 5f22b414c..8a237a741 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java +++ b/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java @@ -73,6 +73,10 @@ public MethodExceptions popExceptions() { methodExceptions.setTreatmentWithConfig(methodExceptionsCounter.get(Method.TREATMENT_WITH_CONFIG).getAndSet(0L)); methodExceptions.setTreatmentsWithConfig(methodExceptionsCounter.get(Method.TREATMENTS_WITH_CONFIG).getAndSet(0L)); methodExceptions.setTrack(methodExceptionsCounter.get(Method.TRACK).getAndSet(0L)); + methodExceptions.setTreatmentsByFlagSet(methodExceptionsCounter.get(Method.TREATMENTS_BY_FLAG_SET).getAndSet(0L)); + methodExceptions.setTreatmentsByFlagSets(methodExceptionsCounter.get(Method.TREATMENTS_BY_FLAG_SETS).getAndSet(0L)); + methodExceptions.setTreatmentsWithConfigByFlagSet(methodExceptionsCounter.get(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET).getAndSet(0L)); + methodExceptions.setTreatmentsWithConfigByFlagSets(methodExceptionsCounter.get(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS).getAndSet(0L)); return methodExceptions; } @@ -86,6 +90,10 @@ public MethodLatencies popLatencies() { latencies.setTreatments(popLatencies(Method.TREATMENTS)); latencies.setTreatmentWithConfig(popLatencies(Method.TREATMENT_WITH_CONFIG)); latencies.setTreatmentsWithConfig(popLatencies(Method.TREATMENTS_WITH_CONFIG)); + latencies.setTreatmentsByFlagSet(popLatencies(Method.TREATMENTS_BY_FLAG_SET)); + latencies.setTreatmentsByFlagSets(popLatencies(Method.TREATMENTS_BY_FLAG_SETS)); + latencies.setTreatmentsWithConfigByFlagSet(popLatencies(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET)); + latencies.setTreatmentsWithConfigByFlagSets(popLatencies(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)); latencies.setTrack(popLatencies(Method.TRACK)); return latencies; @@ -354,7 +362,7 @@ public void recordUpdatesFromSSE(UpdatesFromSSEEnum sseUpdate) { private void initializeProperties() { initializeMethodExceptionsCounter(); - initializeHttpLatenciesCounter(); + initializeMethodLatenciesCounter(); initializeFactoryCounters(); initializeImpressionsData(); initializeEventsData(); @@ -365,12 +373,16 @@ private void initializeProperties() { initializeUpdatesFromSSE(); } - private void initializeHttpLatenciesCounter() { + private void initializeMethodLatenciesCounter() { methodLatencies.put(Method.TREATMENT, new BinarySearchLatencyTracker()); methodLatencies.put(Method.TREATMENTS, new BinarySearchLatencyTracker()); methodLatencies.put(Method.TREATMENT_WITH_CONFIG, new BinarySearchLatencyTracker()); methodLatencies.put(Method.TREATMENTS_WITH_CONFIG, new BinarySearchLatencyTracker()); methodLatencies.put(Method.TRACK, new BinarySearchLatencyTracker()); + methodLatencies.put(Method.TREATMENTS_BY_FLAG_SET, new BinarySearchLatencyTracker()); + methodLatencies.put(Method.TREATMENTS_BY_FLAG_SETS, new BinarySearchLatencyTracker()); + methodLatencies.put(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, new BinarySearchLatencyTracker()); + methodLatencies.put(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, new BinarySearchLatencyTracker()); } private void initializeMethodExceptionsCounter() { @@ -379,6 +391,10 @@ private void initializeMethodExceptionsCounter() { methodExceptionsCounter.put(Method.TREATMENT_WITH_CONFIG, new AtomicLong()); methodExceptionsCounter.put(Method.TREATMENTS_WITH_CONFIG, new AtomicLong()); methodExceptionsCounter.put(Method.TRACK, new AtomicLong()); + methodExceptionsCounter.put(Method.TREATMENTS_BY_FLAG_SET, new AtomicLong()); + methodExceptionsCounter.put(Method.TREATMENTS_BY_FLAG_SETS, new AtomicLong()); + methodExceptionsCounter.put(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, new AtomicLong()); + methodExceptionsCounter.put(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, new AtomicLong()); } private void initializeFactoryCounters() { diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java b/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java index adfb6f97f..1e98944f2 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java +++ b/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java @@ -20,11 +20,17 @@ public class TelemetryConfigProviderImpl implements TelemetryConfigProvider { private final TelemetryStorageConsumer mTelemetryConsumer; private final SplitClientConfig mSplitClientConfig; + private final int mValidFlagSetCount; + private final int mInvalidFlagSetCount; public TelemetryConfigProviderImpl(@NonNull TelemetryStorageConsumer telemetryConsumer, - @NonNull SplitClientConfig splitClientConfig) { + @NonNull SplitClientConfig splitClientConfig, + int validFlagSetCount, + int invalidFlagSetCount) { mTelemetryConsumer = checkNotNull(telemetryConsumer); mSplitClientConfig = checkNotNull(splitClientConfig); + mValidFlagSetCount = validFlagSetCount; + mInvalidFlagSetCount = invalidFlagSetCount; } @Override @@ -44,6 +50,8 @@ public Config getConfigTelemetry() { config.setImpressionsQueueSize(mSplitClientConfig.impressionsQueueSize()); config.setEventsQueueSize(mSplitClientConfig.eventsQueueSize()); config.setUserConsent(mSplitClientConfig.userConsent().intValue()); + config.setFlagSetsTotal(mValidFlagSetCount + mInvalidFlagSetCount); + config.setFlagSetsInvalid(mInvalidFlagSetCount); if (mSplitClientConfig.impressionsMode() == ImpressionsMode.DEBUG) { config.setImpressionsMode(io.split.android.client.telemetry.model.ImpressionsMode.DEBUG.intValue()); } else if (mSplitClientConfig.impressionsMode() == ImpressionsMode.OPTIMIZED) { diff --git a/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java b/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java new file mode 100644 index 000000000..57642a6ec --- /dev/null +++ b/src/main/java/io/split/android/client/validators/FlagSetsValidatorImpl.java @@ -0,0 +1,102 @@ +package io.split.android.client.validators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.utils.logger.Logger; + +public class FlagSetsValidatorImpl implements SplitFilterValidator { + + private static final String FLAG_SET_REGEX = "^[a-z0-9][_a-z0-9]{0,49}$"; + + /** + * Validates the flag sets and returns a list of + * de-duplicated and alphanumerically ordered valid flag sets. + * + * @param values list of flag sets + * @return list of unique alphanumerically ordered valid flag sets + */ + @Override + public ValidationResult cleanup(String method, List values) { + if (values == null || values.isEmpty()) { + return new ValidationResult(Collections.emptyList(), 0); + } + + int invalidValueCount = 0; + + TreeSet cleanedUpSets = new TreeSet<>(); + for (String set : values) { + if (set == null || set.isEmpty()) { + invalidValueCount++; + continue; + } + + if (set.trim().length() != set.length()) { + Logger.w(method + ": Flag Set name " + set + " has extra whitespace, trimming"); + set = set.trim(); + } + + if (!set.toLowerCase().equals(set)) { + Logger.w(method + ": Flag Set name "+set+" should be all lowercase - converting string to lowercase"); + set = set.toLowerCase(); + } + + if (set.matches(FLAG_SET_REGEX)) { + if (!cleanedUpSets.add(set)) { + Logger.w(method + ": you passed duplicated Flag Set. " + set + " was deduplicated"); + invalidValueCount++; + } + } else { + invalidValueCount++; + Logger.w(method + ": you passed "+ set +", Flag Set must adhere to the regular expressions "+ FLAG_SET_REGEX +". This means a Flag Set must be start with a letter, be in lowercase, alphanumeric and have a max length of 50 characters. "+ set +" was discarded."); + } + } + + return new ValidationResult(new ArrayList<>(cleanedUpSets), invalidValueCount); + } + + @Override + public boolean isValid(String value) { + return value != null && value.trim().matches(FLAG_SET_REGEX); + } + + @Override + public Set items(String method, List values, FlagSetsFilter flagSetsFilter) { + Set setsToReturn = new HashSet<>(); + + if (values == null || values.isEmpty()) { + return setsToReturn; + } + + for (String flagSet : values) { + if (flagSet.trim().length() != flagSet.length()) { + Logger.w(method + ": Flag Set name " + flagSet + " has extra whitespace, trimming"); + flagSet = flagSet.trim(); + } + + if (!flagSet.toLowerCase().equals(flagSet)) { + Logger.w(method + ": Flag Set name "+flagSet+" should be all lowercase - converting string to lowercase"); + flagSet = flagSet.toLowerCase(); + } + + if (!isValid(flagSet)) { + Logger.w(method + ": you passed "+ flagSet +", Flag Set must adhere to the regular expressions "+ FLAG_SET_REGEX +". This means a Flag Set must be start with a letter, be in lowercase, alphanumeric and have a max length of 50 characters. "+ flagSet +" was discarded."); + continue; + } + + if (flagSetsFilter != null && !flagSetsFilter.intersect(flagSet)) { + Logger.w(method + ": you passed Flag Set: "+ flagSet +" and is not part of the configured Flag set list, ignoring the request."); + continue; + } + + setsToReturn.add(flagSet); + } + + return setsToReturn; + } +} diff --git a/src/main/java/io/split/android/client/validators/SplitFilterValidator.java b/src/main/java/io/split/android/client/validators/SplitFilterValidator.java new file mode 100644 index 000000000..1e3255b84 --- /dev/null +++ b/src/main/java/io/split/android/client/validators/SplitFilterValidator.java @@ -0,0 +1,35 @@ +package io.split.android.client.validators; + +import java.util.List; +import java.util.Set; + +import io.split.android.client.FlagSetsFilter; + +public interface SplitFilterValidator { + + ValidationResult cleanup(String method, List values); + + boolean isValid(String value); + + Set items(String method, List values, FlagSetsFilter flagSetsFilter); + + class ValidationResult { + + private final List mValues; + + private final int mInvalidValueCount; + + public ValidationResult(List values, int invalidValueCount) { + mValues = values; + mInvalidValueCount = invalidValueCount; + } + + public List getValues() { + return mValues; + } + + public int getInvalidValueCount() { + return mInvalidValueCount; + } + } +} diff --git a/src/main/java/io/split/android/client/validators/TreatmentManager.java b/src/main/java/io/split/android/client/validators/TreatmentManager.java index 0f0d87895..fbb790052 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManager.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManager.java @@ -1,5 +1,8 @@ package io.split.android.client.validators; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.List; import java.util.Map; import io.split.android.client.SplitResult; @@ -13,4 +16,12 @@ public interface TreatmentManager { Map getTreatments(List splits, Map attributes, boolean isClientDestroyed); Map getTreatmentsWithConfig(List splits, Map attributes, boolean isClientDestroyed); + + Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed); + + Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed); + + Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed); + + Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed); } diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java b/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java index be6309fb0..1aa809c4e 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java @@ -3,15 +3,19 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.split.android.client.Evaluator; +import io.split.android.client.EvaluatorImpl; +import io.split.android.client.FlagSetsFilter; import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; -import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.engine.experiments.SplitParser; public class TreatmentManagerFactoryImpl implements TreatmentManagerFactory { @@ -22,6 +26,9 @@ public class TreatmentManagerFactoryImpl implements TreatmentManagerFactory { private final AttributesMerger mAttributesMerger; private final TelemetryStorageProducer mTelemetryStorageProducer; private final Evaluator mEvaluator; + private final FlagSetsFilter mFlagSetsFilter; + private final SplitsStorage mSplitsStorage; + private final ValidationMessageLogger mValidationMessageLogger; public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator, @NonNull SplitValidator splitValidator, @@ -29,14 +36,19 @@ public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator, boolean labelsEnabled, @NonNull AttributesMerger attributesMerger, @NonNull TelemetryStorageProducer telemetryStorageProducer, - @NonNull Evaluator evaluator) { + @NonNull SplitParser splitParser, + @Nullable FlagSetsFilter flagSetsFilter, + @NonNull SplitsStorage splitsStorage) { mKeyValidator = checkNotNull(keyValidator); mSplitValidator = checkNotNull(splitValidator); mCustomerImpressionListener = checkNotNull(customerImpressionListener); mLabelsEnabled = labelsEnabled; mAttributesMerger = checkNotNull(attributesMerger); mTelemetryStorageProducer = checkNotNull(telemetryStorageProducer); - mEvaluator = checkNotNull(evaluator); + mEvaluator = new EvaluatorImpl(splitsStorage, splitParser); + mFlagSetsFilter = flagSetsFilter; + mSplitsStorage = checkNotNull(splitsStorage); + mValidationMessageLogger = new ValidationMessageLoggerImpl(); } @Override @@ -52,7 +64,10 @@ public TreatmentManager getTreatmentManager(Key key, ListenableEventsManager eve eventsManager, attributesManager, mAttributesMerger, - mTelemetryStorageProducer + mTelemetryStorageProducer, + mFlagSetsFilter, + mSplitsStorage, + mValidationMessageLogger ); } } diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java b/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java index 8cc4b55b4..05469a2bf 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java @@ -3,13 +3,19 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import io.split.android.client.EvaluationResult; import io.split.android.client.Evaluator; +import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitResult; import io.split.android.client.TreatmentLabels; import io.split.android.client.attributes.AttributesManager; @@ -18,6 +24,7 @@ import io.split.android.client.events.SplitEvent; import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.Method; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.utils.logger.Logger; @@ -30,6 +37,10 @@ private static class ValidationTag { public static final String GET_TREATMENTS = "getTreatments"; public static final String GET_TREATMENT_WITH_CONFIG = "getTreatmentWithConfig"; public static final String GET_TREATMENTS_WITH_CONFIG = "getTreatmentsWithConfig"; + public static final String GET_TREATMENTS_BY_FLAG_SET = "getTreatmentsByFlagSet"; + public static final String GET_TREATMENTS_BY_FLAG_SETS = "getTreatmentsByFlagSets"; + public static final String GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET = "getTreatmentsWithConfigByFlagSet"; + public static final String GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = "getTreatmentsWithConfigByFlagSets"; } private final String CLIENT_DESTROYED_MESSAGE = "Client has already been destroyed - no calls possible"; @@ -48,6 +59,9 @@ private static class ValidationTag { @NonNull private final AttributesMerger mAttributesMerger; private final TelemetryStorageProducer mTelemetryStorageProducer; + private final FlagSetsFilter mFlagSetsFilter; + private final SplitsStorage mSplitsStorage; + private final SplitFilterValidator mFlagSetsValidator; public TreatmentManagerImpl(String matchingKey, String bucketingKey, @@ -59,7 +73,10 @@ public TreatmentManagerImpl(String matchingKey, ListenableEventsManager eventsManager, @NonNull AttributesManager attributesManager, @NonNull AttributesMerger attributesMerger, - @NonNull TelemetryStorageProducer telemetryStorageProducer) { + @NonNull TelemetryStorageProducer telemetryStorageProducer, + @Nullable FlagSetsFilter flagSetsFilter, + @NonNull SplitsStorage splitsStorage, + @NonNull ValidationMessageLogger validationLogger) { mEvaluator = evaluator; mKeyValidator = keyValidator; mSplitValidator = splitValidator; @@ -68,10 +85,13 @@ public TreatmentManagerImpl(String matchingKey, mImpressionListener = impressionListener; mLabelsEnabled = labelsEnabled; mEventsManager = eventsManager; - mValidationLogger = new ValidationMessageLoggerImpl(); + mValidationLogger = checkNotNull(validationLogger); mAttributesManager = checkNotNull(attributesManager); mAttributesMerger = checkNotNull(attributesMerger); mTelemetryStorageProducer = checkNotNull(telemetryStorageProducer); + mFlagSetsFilter = flagSetsFilter; + mSplitsStorage = checkNotNull(splitsStorage); + mFlagSetsValidator = new FlagSetsValidatorImpl(); } @Override @@ -162,6 +182,106 @@ public Map getTreatmentsWithConfig(List splits, Map return result; } + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed) { + String validationTag = ValidationTag.GET_TREATMENTS_BY_FLAG_SET; + Set names = new HashSet<>(); + try { + if (isClientDestroyed) { + mValidationLogger.e(CLIENT_DESTROYED_MESSAGE, validationTag); + return controlTreatmentsForSplits(new ArrayList<>(names), validationTag); + } + names = getNamesFromSet("getTreatmentsByFlagSet", Collections.singletonList(flagSet)); + + long start = System.currentTimeMillis(); + try { + return evaluateFeatures(names, attributes, validationTag, SplitResult::treatment); + } finally { + recordLatency(Method.TREATMENTS_BY_FLAG_SET, start); + } + } catch (Exception exception) { + Logger.e("Client getTreatmentsByFlagSet exception", exception); + mTelemetryStorageProducer.recordException(Method.TREATMENTS_BY_FLAG_SET); + + return controlTreatmentsForSplits(new ArrayList<>(names), validationTag); + } + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed) { + String validationTag = ValidationTag.GET_TREATMENTS_BY_FLAG_SETS; + Set names = new HashSet<>(); + try { + if (isClientDestroyed) { + mValidationLogger.e(CLIENT_DESTROYED_MESSAGE, validationTag); + return controlTreatmentsForSplits(new ArrayList<>(names), validationTag); + } + names = getNamesFromSet("getTreatmentsByFlagSets", flagSets); + + long start = System.currentTimeMillis(); + try { + return evaluateFeatures(names, attributes, validationTag, SplitResult::treatment); + } finally { + recordLatency(Method.TREATMENTS_BY_FLAG_SETS, start); + } + } catch (Exception exception) { + Logger.e("Client getTreatmentsByFlagSets exception", exception); + mTelemetryStorageProducer.recordException(Method.TREATMENTS_BY_FLAG_SETS); + + return controlTreatmentsForSplits(new ArrayList<>(names), validationTag); + } + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed) { + String validationTag = ValidationTag.GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET; + Set names = new HashSet<>(); + try { + names = getNamesFromSet("getTreatmentsWithConfigByFlagSet", Collections.singletonList(flagSet)); + if (isClientDestroyed) { + mValidationLogger.e(CLIENT_DESTROYED_MESSAGE, validationTag); + return controlTreatmentsForSplitsWithConfig(new ArrayList<>(names), validationTag); + } + + long start = System.currentTimeMillis(); + try { + return evaluateFeatures(names, attributes, validationTag, ResultTransformer::identity); + } finally { + recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, start); + } + } catch (Exception exception) { + Logger.e("Client getTreatmentsWithConfigByFlagSet exception", exception); + mTelemetryStorageProducer.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET); + + return controlTreatmentsForSplitsWithConfig(new ArrayList<>(names), validationTag); + } + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed) { + String validationTag = ValidationTag.GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS; + Set names = new HashSet<>(); + try { + if (isClientDestroyed) { + mValidationLogger.e(CLIENT_DESTROYED_MESSAGE, validationTag); + return controlTreatmentsForSplitsWithConfig(new ArrayList<>(names), validationTag); + } + names = getNamesFromSet("getTreatmentsWithConfigByFlagSets", flagSets); + + long start = System.currentTimeMillis(); + try { + return evaluateFeatures(names, attributes, validationTag, ResultTransformer::identity); + } finally { + recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, start); + } + } catch (Exception exception) { + Logger.e("Client getTreatmentsWithConfigByFlagSets exception", exception); + mTelemetryStorageProducer.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); + + return controlTreatmentsForSplitsWithConfig(new ArrayList<>(names), validationTag); + } + } + private SplitResult getTreatmentWithConfigWithoutMetrics(String split, Map attributes, String validationTag) { ValidationErrorInfo errorInfo = mKeyValidator.validate(mMatchingKey, mBucketingKey); @@ -266,19 +386,49 @@ private Map controlTreatmentsForSplits(List splits, Stri return TreatmentManagerHelper.controlTreatmentsForSplits(splits, mSplitValidator, validationTag, mValidationLogger); } - private EvaluationResult evaluateIfReady(String splitName, + private EvaluationResult evaluateIfReady(String featureFlagName, Map attributes, String validationTag) { if (!mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY) && !mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)) { - mValidationLogger.w("the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method", validationTag); + mValidationLogger.w("the SDK is not ready, results may be incorrect for feature flag " + featureFlagName + ". Make sure to wait for SDK readiness before using this method", validationTag); mTelemetryStorageProducer.recordNonReadyUsage(); return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.NOT_READY, null, null); } - return mEvaluator.getTreatment(mMatchingKey, mBucketingKey, splitName, attributes); + return mEvaluator.getTreatment(mMatchingKey, mBucketingKey, featureFlagName, attributes); } private void recordLatency(Method treatment, long startTime) { mTelemetryStorageProducer.recordLatency(treatment, System.currentTimeMillis() - startTime); } + + @NonNull + private Set getNamesFromSet(@NonNull String method, @NonNull List flagSets) { + + Set setsToEvaluate = mFlagSetsValidator.items(method, flagSets, mFlagSetsFilter); + + if (setsToEvaluate.isEmpty()) { + return new HashSet<>(); + } + + return mSplitsStorage.getNamesByFlagSets(setsToEvaluate); + } + + private Map evaluateFeatures(Set names, @Nullable Map attributes, String validationTag, ResultTransformer transformer) { + Map result = new HashMap<>(); + for (String featureFlagName : names) { + SplitResult splitResult = getTreatmentWithConfigWithoutMetrics(featureFlagName, attributes, validationTag); + result.put(featureFlagName, transformer.transform(splitResult)); + } + return result; + } + + private interface ResultTransformer { + + T transform(SplitResult splitResult); + + static SplitResult identity(SplitResult splitResult) { + return splitResult; + } + } } diff --git a/src/main/java/io/split/android/engine/experiments/ParsedSplit.java b/src/main/java/io/split/android/engine/experiments/ParsedSplit.java index db800870a..1661d3cdf 100644 --- a/src/main/java/io/split/android/engine/experiments/ParsedSplit.java +++ b/src/main/java/io/split/android/engine/experiments/ParsedSplit.java @@ -1,30 +1,28 @@ package io.split.android.engine.experiments; +import androidx.annotation.NonNull; + import com.google.common.collect.ImmutableList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; -/** - * a value class representing an io.codigo.dtos.Experiment. Why are we not using - * that class? Because it does not have the logic of matching. ParsedExperiment - * has the matchers that also encapsulate the logic of matching. We - * can easily cache this object. - */ -@SuppressWarnings("RedundantCast") public class ParsedSplit { - private final String _split; - private final int _seed; - private final boolean _killed; - private final String _defaultTreatment; - private final ImmutableList _parsedCondition; - private final String _trafficTypeName; - private final long _changeNumber; - private final int _trafficAllocation; - private final int _trafficAllocationSeed; - private final int _algo; - private final Map _configurations; + private final String mSplit; + private final int mSeed; + private final boolean mKilled; + private final String mDefaultTreatment; + private final ImmutableList mParsedCondition; + private final String mTrafficTypeName; + private final long mChangeNumber; + private final int mTrafficAllocation; + private final int mTrafficAllocationSeed; + private final int mAlgo; + private final Map mConfigurations; + private final Set mSets; public ParsedSplit( String feature, @@ -37,81 +35,87 @@ public ParsedSplit( int trafficAllocation, int trafficAllocationSeed, int algo, - Map configurations + Map configurations, + Set sets ) { - _split = feature; - _seed = seed; - _killed = killed; - _defaultTreatment = defaultTreatment; - _parsedCondition = ImmutableList.copyOf(matcherAndSplits); - _trafficTypeName = trafficTypeName; - _changeNumber = changeNumber; - _algo = algo; - _configurations = configurations; - - if (_defaultTreatment == null) { + mSplit = feature; + mSeed = seed; + mKilled = killed; + mDefaultTreatment = defaultTreatment; + mParsedCondition = ImmutableList.copyOf(matcherAndSplits); + mTrafficTypeName = trafficTypeName; + mChangeNumber = changeNumber; + mAlgo = algo; + mConfigurations = configurations; + + if (mDefaultTreatment == null) { throw new IllegalArgumentException("DefaultTreatment is null"); } - this._trafficAllocation = trafficAllocation; - this._trafficAllocationSeed = trafficAllocationSeed; + mTrafficAllocation = trafficAllocation; + mTrafficAllocationSeed = trafficAllocationSeed; + mSets = sets; } - public String feature() { - return _split; + return mSplit; } public int trafficAllocation() { - return _trafficAllocation; + return mTrafficAllocation; } public int trafficAllocationSeed() { - return _trafficAllocationSeed; + return mTrafficAllocationSeed; } public int seed() { - return _seed; + return mSeed; } public boolean killed() { - return _killed; + return mKilled; } public String defaultTreatment() { - return _defaultTreatment; + return mDefaultTreatment; } public List parsedConditions() { - return _parsedCondition; + return mParsedCondition; } public String trafficTypeName() { - return _trafficTypeName; + return mTrafficTypeName; } public long changeNumber() { - return _changeNumber; + return mChangeNumber; } public int algo() { - return _algo; + return mAlgo; } public Map configurations() { - return _configurations; + return mConfigurations; + } + + public Set sets() { + return mSets; } @Override public int hashCode() { int result = 17; - result = 31 * result + _split.hashCode(); - result = 31 * result + (int) (_seed ^ (_seed >>> 32)); - result = 31 * result + (_killed ? 1 : 0); - result = 31 * result + _defaultTreatment.hashCode(); - result = 31 * result + _parsedCondition.hashCode(); - result = 31 * result + (_trafficTypeName == null ? 0 : _trafficTypeName.hashCode()); - result = 31 * result + (int) (_changeNumber ^ (_changeNumber >>> 32)); - result = 31 * result + (_algo ^ (_algo >>> 32)); + result = 31 * result + mSplit.hashCode(); + result = 31 * result + (int) (mSeed ^ (mSeed >>> 32)); + result = 31 * result + (mKilled ? 1 : 0); + result = 31 * result + mDefaultTreatment.hashCode(); + result = 31 * result + mParsedCondition.hashCode(); + result = 31 * result + (mTrafficTypeName == null ? 0 : mTrafficTypeName.hashCode()); + result = 31 * result + (int) (mChangeNumber ^ (mChangeNumber >>> 32)); + result = 31 * result + (mAlgo ^ (mAlgo >>> 32)); + result = 31 * result + ((mSets != null) ? mSets.hashCode() : 0); return result; } @@ -122,25 +126,27 @@ public boolean equals(Object obj) { if (!(obj instanceof ParsedSplit)) return false; ParsedSplit other = (ParsedSplit) obj; - return _split.equals(other._split) - && _seed == other._seed - && _killed == other._killed - && _defaultTreatment.equals(other._defaultTreatment) - && _parsedCondition.equals(other._parsedCondition) - && (_trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName)) - && _changeNumber == other._changeNumber - && _algo == other._algo - && (_configurations == null ? other._configurations == null : _configurations.equals(other._configurations)); + return mSplit.equals(other.mSplit) + && mSeed == other.mSeed + && mKilled == other.mKilled + && mDefaultTreatment.equals(other.mDefaultTreatment) + && mParsedCondition.equals(other.mParsedCondition) + && (Objects.equals(mTrafficTypeName, other.mTrafficTypeName)) + && mChangeNumber == other.mChangeNumber + && mAlgo == other.mAlgo + && (Objects.equals(mConfigurations, other.mConfigurations)) + && (Objects.equals(mSets, other.mSets)); } + @NonNull @Override public String toString() { - return "name:" + _split + ", seed:" + _seed + ", killed:" + _killed + - ", default treatment:" + _defaultTreatment + - ", parsedConditions:" + _parsedCondition + - ", trafficTypeName:" + _trafficTypeName + ", changeNumber:" + _changeNumber + - ", algo:" + _algo + ", config:" + _configurations; + return "name:" + mSplit + ", seed:" + mSeed + ", killed:" + mKilled + + ", default treatment:" + mDefaultTreatment + + ", parsedConditions:" + mParsedCondition + + ", trafficTypeName:" + mTrafficTypeName + ", changeNumber:" + mChangeNumber + + ", algo:" + mAlgo + ", config:" + mConfigurations + ", sets:" + mSets; } } diff --git a/src/main/java/io/split/android/engine/experiments/SplitParser.java b/src/main/java/io/split/android/engine/experiments/SplitParser.java index f07087e0f..274afbe0d 100644 --- a/src/main/java/io/split/android/engine/experiments/SplitParser.java +++ b/src/main/java/io/split/android/engine/experiments/SplitParser.java @@ -62,7 +62,7 @@ public ParsedSplit parse(@Nullable Split split, @Nullable String matchingKey) { try { return parseWithoutExceptionHandling(split, matchingKey); } catch (Throwable t) { - Logger.e(t, "Could not parse feature flag: %s", split); + Logger.e(t, "Could not parse feature flag: %s", (split != null) ? split.name : "null"); return null; } } @@ -90,7 +90,18 @@ private ParsedSplit parseWithoutExceptionHandling(Split split, String matchingKe parsedConditionList.add(new ParsedCondition(condition.conditionType, matcher, partitions, condition.label)); } - return new ParsedSplit(split.name, split.seed, split.killed, split.defaultTreatment, parsedConditionList, split.trafficTypeName, split.changeNumber, split.trafficAllocation, split.trafficAllocationSeed, split.algo, split.configurations); + return new ParsedSplit(split.name, + split.seed, + split.killed, + split.defaultTreatment, + parsedConditionList, + split.trafficTypeName, + split.changeNumber, + split.trafficAllocation, + split.trafficAllocationSeed, + split.algo, + split.configurations, + split.sets); } private CombiningMatcher toMatcher(MatcherGroup matcherGroup, String matchingKey) { diff --git a/src/test/java/io/split/android/client/FilterBuilderTest.java b/src/test/java/io/split/android/client/FilterBuilderTest.java index 48104e658..898999535 100644 --- a/src/test/java/io/split/android/client/FilterBuilderTest.java +++ b/src/test/java/io/split/android/client/FilterBuilderTest.java @@ -1,10 +1,15 @@ package io.split.android.client; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; public class FilterBuilderTest { @@ -16,9 +21,9 @@ public void testBasicQueryString() { SplitFilter byNameFilter = SplitFilter.byName(Arrays.asList("nf_a", "nf_c", "nf_b")); SplitFilter byPrefixFilter = SplitFilter.byPrefix(Arrays.asList("pf_c", "pf_b", "pf_a")); - String queryString = new FilterBuilder().addFilters(Arrays.asList(byNameFilter, byPrefixFilter)).build(); + String queryString = new FilterBuilder(Arrays.asList(byNameFilter, byPrefixFilter)).buildQueryString(); - Assert.assertEquals("&names=nf_a,nf_b,nf_c&prefixes=pf_a,pf_b,pf_c", queryString); + assertEquals("&names=nf_a,nf_b,nf_c&prefixes=pf_a,pf_b,pf_c", queryString); } @Test @@ -28,16 +33,16 @@ public void testOnlyOneTypeQueryString() { SplitFilter byNameFilter = SplitFilter.byName(Arrays.asList("nf_a", "nf_c", "nf_b")); SplitFilter byPrefixFilter = SplitFilter.byPrefix(Arrays.asList("pf_c", "pf_b", "pf_a")); - String onlyByNameQs = new FilterBuilder().addFilters(Arrays.asList(byNameFilter)).build(); - String onlyByPrefixQs = new FilterBuilder().addFilters(Arrays.asList(byPrefixFilter)).build(); + String onlyByNameQs = new FilterBuilder(Arrays.asList(byNameFilter)).buildQueryString(); + String onlyByPrefixQs = new FilterBuilder(Arrays.asList(byPrefixFilter)).buildQueryString(); - Assert.assertEquals("&names=nf_a,nf_b,nf_c", onlyByNameQs); - Assert.assertEquals("&prefixes=pf_a,pf_b,pf_c", onlyByPrefixQs); + assertEquals("&names=nf_a,nf_b,nf_c", onlyByNameQs); + assertEquals("&prefixes=pf_a,pf_b,pf_c", onlyByPrefixQs); } @Test public void filterValuesDeduptedAndGrouped() { - // Duplicated filter values should be removed on builing + // Duplicated filter values should be removed on building List filters = Arrays.asList( SplitFilter.byName(Arrays.asList("nf_a", "nf_c", "nf_b")), @@ -45,11 +50,10 @@ public void filterValuesDeduptedAndGrouped() { SplitFilter.byPrefix(Arrays.asList("pf_a", "pf_c", "pf_b")), SplitFilter.byPrefix(Arrays.asList("pf_d", "pf_a"))); - String queryString = new FilterBuilder() - .addFilters(filters) - .build(); + String queryString = new FilterBuilder(filters) + .buildQueryString(); - Assert.assertEquals("&names=nf_a,nf_b,nf_c,nf_d&prefixes=pf_a,pf_b,pf_c,pf_d", queryString); + assertEquals("&names=nf_a,nf_b,nf_c,nf_d&prefixes=pf_a,pf_b,pf_c,pf_d", queryString); } @Test @@ -63,9 +67,8 @@ public void maxByNameFilterExceded() { } try { - String queryString = new FilterBuilder() - .addFilters(Arrays.asList(SplitFilter.byName(values))) - .build(); + String queryString = new FilterBuilder(Arrays.asList(SplitFilter.byName(values))) + .buildQueryString(); } catch (Exception e) { exceptionThrown = true; } @@ -84,9 +87,8 @@ public void maxByPrefixFilterExceded() { } try { - String queryString = new FilterBuilder() - .addFilters(Arrays.asList(SplitFilter.byPrefix(values))) - .build(); + String queryString = new FilterBuilder(Arrays.asList(SplitFilter.byPrefix(values))) + .buildQueryString(); } catch (Exception e) { exceptionThrown = true; } @@ -98,9 +100,9 @@ public void maxByPrefixFilterExceded() { public void testNoFilters() { // When no filter added, query string has to be empty - String queryString = new FilterBuilder().build(); + String queryString = new FilterBuilder(Collections.emptyList()).buildQueryString(); - Assert.assertEquals("", queryString); + assertEquals("", queryString); } @Test @@ -110,8 +112,8 @@ public void testQueryStringWithSpecialChars1() { .addSplitFilter(SplitFilter.byName(Arrays.asList("ausgefüllt"))) .addSplitFilter(SplitFilter.byPrefix(Arrays.asList())) .build(); - String queryString = new FilterBuilder().addFilters(config.getFilters()).build(); - Assert.assertEquals("&names=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); + String queryString = new FilterBuilder(config.getFilters()).buildQueryString(); + assertEquals("&names=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); } @Test @@ -121,8 +123,8 @@ public void testQueryStringWithSpecialChars2() { .addSplitFilter(SplitFilter.byPrefix(Arrays.asList("ausgefüllt"))) .addSplitFilter(SplitFilter.byName(Arrays.asList())) .build(); - String queryString = new FilterBuilder().addFilters(config.getFilters()).build(); - Assert.assertEquals("&prefixes=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); + String queryString = new FilterBuilder(config.getFilters()).buildQueryString(); + assertEquals("&prefixes=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); } @Test @@ -133,8 +135,8 @@ public void testQueryStringWithSpecialChars3() { .addSplitFilter(SplitFilter.byPrefix(Arrays.asList("\u0223abc", "abc\u0223asd", "abc\u0223"))) .addSplitFilter(SplitFilter.byPrefix(Arrays.asList("ausgefüllt"))) .build(); - String queryString = new FilterBuilder().addFilters(config.getFilters()).build(); - Assert.assertEquals("&names=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc&prefixes=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); + String queryString = new FilterBuilder(config.getFilters()).buildQueryString(); + assertEquals("&names=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc&prefixes=abc\u0223,abc\u0223asd,ausgefüllt,\u0223abc", queryString); } @Test @@ -142,7 +144,50 @@ public void testQueryStringWithSpecialChars4() { SyncConfig config = SyncConfig.builder() .addSplitFilter(SplitFilter.byName(Arrays.asList("__ш", "__a", "%", "%25", " __ш ", "% "))) .build(); - String queryString = new FilterBuilder().addFilters(config.getFilters()).build(); - Assert.assertEquals("&names=%,%25,__a,__ш", queryString); + String queryString = new FilterBuilder(config.getFilters()).buildQueryString(); + assertEquals("&names=%,%25,__a,__ш", queryString); + } + + @Test + public void addingBySetFilterAlongsideOtherTypesLeavesOnlyBySet() { + List filters = Arrays.asList( + SplitFilter.byName(Arrays.asList("nf_a", "nf_c", "nf_b")), + SplitFilter.byName(Arrays.asList("nf_b", "nf_d")), + SplitFilter.bySet(Collections.singletonList("zz")), + SplitFilter.byPrefix(Arrays.asList("pf_a", "pf_c", "pf_b")), + SplitFilter.bySet(Arrays.asList("pf_d", "pf_a", "_invalid"))); + + String queryString = new FilterBuilder(filters).buildQueryString(); + + assertEquals("&sets=pf_a,pf_d,zz", queryString); + } + + @Test + public void bySetQueryStringIsBuiltCorrectly() { + String queryString = new FilterBuilder(Arrays.asList(SplitFilter.bySet(Arrays.asList("pf_d", "pf_a", "_invalid")))).buildQueryString(); + + assertEquals("&sets=pf_a,pf_d", queryString); + } + + @Test + public void addingMultipleBySetFiltersCombinesTheValues() { + List filters = Arrays.asList( + SplitFilter.bySet(Arrays.asList("pf_d", "pf_a", "_invalid")), + SplitFilter.bySet(Arrays.asList("pf_d", "pf_c", "_invalid")), + SplitFilter.bySet(Arrays.asList("zz", "zzz"))); + + String queryString = new FilterBuilder(filters).buildQueryString(); + + assertEquals("&sets=pf_a,pf_c,pf_d,zz,zzz", queryString); + } + + @Test + public void getGroupedFiltersUsesFilterGrouper() { + FilterGrouper filterGrouper = mock(FilterGrouper.class); + FilterBuilder filterBuilder = new FilterBuilder(filterGrouper, Collections.emptyList()); + + filterBuilder.getGroupedFilter(); + + verify(filterGrouper).group(Collections.emptyList()); } } diff --git a/src/test/java/io/split/android/client/FilterGrouperTest.java b/src/test/java/io/split/android/client/FilterGrouperTest.java index c5b5e6868..e3a7fbcec 100644 --- a/src/test/java/io/split/android/client/FilterGrouperTest.java +++ b/src/test/java/io/split/android/client/FilterGrouperTest.java @@ -1,11 +1,16 @@ package io.split.android.client; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; public class FilterGrouperTest { @@ -13,18 +18,23 @@ public class FilterGrouperTest { @Test public void groupingFilters() { - List ungropedFilters = new ArrayList<>(); - ungropedFilters.add(SplitFilter.byName(Arrays.asList("f1", "f2", "f3"))); - ungropedFilters.add(SplitFilter.byName(Arrays.asList("f2", "f3", "f4"))); - ungropedFilters.add(SplitFilter.byName(Arrays.asList("f4", "f5", "f6"))); - ungropedFilters.add(SplitFilter.byPrefix(Arrays.asList("f1", "f2", "f3"))); - ungropedFilters.add(SplitFilter.byPrefix(Arrays.asList("f2", "f3", "f4"))); - ungropedFilters.add(SplitFilter.byPrefix(Arrays.asList("f4", "f5", "f6"))); - - List groupedFiltes = mFilterGrouper.group(ungropedFilters); - - /// This compoe - Assert.assertEquals(2, groupedFiltes.size()); + List ungroupedFilters = new ArrayList<>(); + ungroupedFilters.add(SplitFilter.byName(Arrays.asList("f1", "f2", "f3"))); + ungroupedFilters.add(SplitFilter.byName(Arrays.asList("f2", "f3", "f4"))); + ungroupedFilters.add(SplitFilter.byName(Arrays.asList("f4", "f5", "f6"))); + ungroupedFilters.add(SplitFilter.byPrefix(Arrays.asList("f1", "f2", "f3"))); + ungroupedFilters.add(SplitFilter.byPrefix(Arrays.asList("f2", "f3", "f4"))); + ungroupedFilters.add(SplitFilter.byPrefix(Arrays.asList("f4", "f5", "f6"))); + ungroupedFilters.add(SplitFilter.bySet(Arrays.asList("f1", "f2", "f3"))); + ungroupedFilters.add(SplitFilter.bySet(Arrays.asList("f2", "f3", "f4"))); + ungroupedFilters.add(SplitFilter.bySet(Arrays.asList("f4", "f5", "f6"))); + + Map groupedFilters = mFilterGrouper.group(ungroupedFilters); + + // this class only merges filters of the same type + assertEquals(3, groupedFilters.size()); + assertTrue(groupedFilters.containsKey(SplitFilter.Type.BY_NAME)); + assertTrue(groupedFilters.containsKey(SplitFilter.Type.BY_PREFIX)); + assertTrue(groupedFilters.containsKey(SplitFilter.Type.BY_SET)); } - } diff --git a/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java b/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java new file mode 100644 index 000000000..97e68c155 --- /dev/null +++ b/src/test/java/io/split/android/client/FlagSetsFilterImplTest.java @@ -0,0 +1,73 @@ +package io.split.android.client; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +public class FlagSetsFilterImplTest { + + + @Test + public void intersectReturnsTrueWhenShouldFilterIsFalse() { + Set flagSets = new HashSet<>(); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + assertTrue(filter.intersect(new HashSet<>())); + } + + @Test + public void intersectReturnsTrueWhenSetsIsNull() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + assertFalse(filter.intersect((Set) null)); + } + + @Test + public void intersectReturnsTrueWhenSetIsContained() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + Set testSet = new HashSet<>(); + testSet.add("test"); + assertTrue(filter.intersect(testSet)); + } + + @Test + public void intersectReturnsFalseWhenSetIsNotContained() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + Set testSet = new HashSet<>(); + testSet.add("other"); + assertFalse(filter.intersect(testSet)); + } + + @Test + public void intersectReturnsTrueWhenStringSetIsNull() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + assertFalse(filter.intersect((String) null)); + } + + @Test + public void intersectReturnsTrueWhenStringSetIsContained() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + assertTrue(filter.intersect("test")); + } + + @Test + public void intersectReturnsFalseWhenStringSetIsNotContained() { + Set flagSets = new HashSet<>(); + flagSets.add("test"); + FlagSetsFilterImpl filter = new FlagSetsFilterImpl(flagSets); + assertFalse(filter.intersect("other")); + } + +} diff --git a/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java b/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java new file mode 100644 index 000000000..a3f621553 --- /dev/null +++ b/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java @@ -0,0 +1,43 @@ +package io.split.android.client; + +import static org.mockito.Mockito.verify; + +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +public class SplitClientImplFlagSetsTest extends SplitClientImplBaseTest { + + @Test + public void getTreatmentsByFlagSetDelegatesToTreatmentManager() { + Map attributes = Collections.singletonMap("key", "value"); + splitClient.getTreatmentsByFlagSet("set", attributes); + + verify(treatmentManager).getTreatmentsByFlagSet("set", attributes, false); + } + + @Test + public void getTreatmentsByFlagSetsDelegatesToTreatmentManager() { + Map attributes = Collections.singletonMap("key", "value"); + splitClient.getTreatmentsByFlagSets(Collections.singletonList("set"), attributes); + + verify(treatmentManager).getTreatmentsByFlagSets(Collections.singletonList("set"), attributes, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetDelegatesToTreatmentManager() { + Map attributes = Collections.singletonMap("key", "value"); + splitClient.getTreatmentsWithConfigByFlagSet("set", attributes); + + verify(treatmentManager).getTreatmentsWithConfigByFlagSet("set", attributes, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsDelegatesToTreatmentManager() { + Map attributes = Collections.singletonMap("key", "value"); + splitClient.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), attributes); + + verify(treatmentManager).getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), attributes, false); + } +} diff --git a/src/test/java/io/split/android/client/SplitManagerImplTest.java b/src/test/java/io/split/android/client/SplitManagerImplTest.java index 06dee96fa..8fedc46ee 100644 --- a/src/test/java/io/split/android/client/SplitManagerImplTest.java +++ b/src/test/java/io/split/android/client/SplitManagerImplTest.java @@ -23,6 +23,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,6 +33,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; @@ -65,7 +68,6 @@ public void splitCallWithNonExistentSplit() { @Test public void splitCallWithExistentSplit() { String existent = "existent"; - SplitFetcher splitFetcher = Mockito.mock(SplitFetcher.class); Map configs = new HashMap<>(); configs.put("off", "{\"f\":\"v\"}"); @@ -101,7 +103,6 @@ public void splitsCallWithNoSplit() { @Test public void splitsCallWithSplit() { - SplitFetcher splitFetcher = Mockito.mock(SplitFetcher.class); Map splitsMap = new HashMap<>(); Split split = SplitHelper.createSplit("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition()), "traffic", 456L, 1, null); splitsMap.put(split.name, split); @@ -128,7 +129,6 @@ public void splitNamesCallWithNoSplit() { @Test public void splitNamesCallWithSplit() { - SplitFetcher splitFetcher = Mockito.mock(SplitFetcher.class); Map splitsMap = new HashMap<>(); Split split = SplitHelper.createSplit("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition()), @@ -142,8 +142,51 @@ public void splitNamesCallWithSplit() { assertThat(splitNames.get(0), is(equalTo(split.name))); } + @Test + public void flagSets() { + Map splitsMap = new HashMap<>(); + Split split = SplitHelper.createSplit("FeatureName", 123, true, + "off", Lists.newArrayList(getTestCondition()), + "traffic", 456L, 1, null); + splitsMap.put(split.name, split); + when(mSplitsStorage.getAll()).thenReturn(splitsMap); + + SplitManager splitManager = mSplitManager; + + List splitNames = splitManager.splits(); + assertEquals(1, splitNames.size()); + assertEquals(split.name, splitNames.get(0).name); + assertEquals(new ArrayList<>(split.sets), splitNames.get(0).sets); + } + + @Test + public void defaultTreatmentIsPresent() { + Split split = SplitHelper.createSplit("FeatureName", 123, true, + "some_treatment", Lists.newArrayList(getTestCondition()), + "traffic", 456L, 1, null); + when(mSplitsStorage.get("FeatureName")).thenReturn(split); + + SplitView featureFlag = mSplitManager.split("FeatureName"); + + assertEquals("some_treatment", featureFlag.defaultTreatment); + } + + @Test + public void defaultTreatmentIsPresentWhenFetchingMultipleSplits() { + Map splitsMap = new HashMap<>(); + Split split = SplitHelper.createSplit("FeatureName", 123, true, + "some_treatment", Lists.newArrayList(getTestCondition()), + "traffic", 456L, 1, null); + splitsMap.put(split.name, split); + when(mSplitsStorage.getAll()).thenReturn(splitsMap); + + List splitNames = mSplitManager.splits(); + + assertEquals(1, splitNames.size()); + assertEquals("some_treatment", splitNames.get(0).defaultTreatment); + } + private Condition getTestCondition() { return SplitHelper.createCondition(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(ConditionsTestUtil.partition("off", 10))); } - } diff --git a/src/test/java/io/split/android/client/SyncConfigTest.java b/src/test/java/io/split/android/client/SyncConfigTest.java index d46b094c4..2356aeb4a 100644 --- a/src/test/java/io/split/android/client/SyncConfigTest.java +++ b/src/test/java/io/split/android/client/SyncConfigTest.java @@ -162,4 +162,16 @@ public void addingNullFilterToConfig() { Assert.assertTrue(exceptionThrown); Assert.assertNull(config); } + + @Test + public void invalidValuesAreTracked() { + // Currently only invalid values for {@link SplitFilter#BY_SET} are tracked, for telemetry + + SyncConfig config = SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList("_f1", "f2", "f3"))) + .addSplitFilter(SplitFilter.bySet(Arrays.asList("f4", "_f5", "_f6", "_f6"))) + .build(); + + Assert.assertEquals(4, config.getInvalidValueCount()); + } } diff --git a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java index 86b8230d0..e3eecafbe 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -15,17 +16,20 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.events.SplitEvent; import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.Method; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.validators.KeyValidator; import io.split.android.client.validators.SplitValidator; import io.split.android.client.validators.TreatmentManagerImpl; +import io.split.android.client.validators.ValidationMessageLoggerImpl; public class TreatmentManagerTelemetryTest { @@ -45,13 +49,17 @@ public class TreatmentManagerTelemetryTest { AttributesMerger attributesMerger; @Mock TelemetryStorageProducer telemetryStorageProducer; + @Mock + private SplitsStorage mSplitsStorage; + private FlagSetsFilter mFlagSetsFilter; private TreatmentManagerImpl treatmentManager; + private AutoCloseable mAutoCloseable; @Before public void setUp() { - MockitoAnnotations.openMocks(this); - + mAutoCloseable = MockitoAnnotations.openMocks(this); + mFlagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); treatmentManager = new TreatmentManagerImpl( "test_key", "test_key", @@ -63,12 +71,22 @@ public void setUp() { eventsManager, attributesManager, attributesMerger, - telemetryStorageProducer - ); + telemetryStorageProducer, + mFlagSetsFilter, + mSplitsStorage, new ValidationMessageLoggerImpl()); when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); } + @After + public void tearDown() { + try { + mAutoCloseable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Test public void getTreatmentRecordsLatencyInTelemetry() { diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/src/test/java/io/split/android/client/TreatmentManagerTest.java index 02ab324e9..1a5ce153c 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTest.java @@ -7,7 +7,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; - import static io.split.android.client.TreatmentLabels.DEFINITION_NOT_FOUND; import com.google.common.base.Strings; @@ -28,7 +27,6 @@ import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.dtos.Split; -import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.events.SplitEvent; import io.split.android.client.impressions.ImpressionListener; @@ -42,6 +40,8 @@ import io.split.android.client.validators.SplitValidatorImpl; import io.split.android.client.validators.TreatmentManager; import io.split.android.client.validators.TreatmentManagerImpl; +import io.split.android.client.validators.ValidationMessageLogger; +import io.split.android.client.validators.ValidationMessageLoggerImpl; import io.split.android.engine.experiments.SplitParser; import io.split.android.fake.ImpressionListenerMock; import io.split.android.fake.SplitEventsManagerStub; @@ -56,10 +56,17 @@ public class TreatmentManagerTest { ListenableEventsManager eventsManagerStub; AttributesManager attributesManager = mock(AttributesManager.class); TelemetryStorageProducer telemetryStorageProducer = mock(TelemetryStorageProducer.class); - TreatmentManagerImpl treatmentManager = initializeTreatmentManager(); + private FlagSetsFilter mFlagSetsFilter; + TreatmentManagerImpl treatmentManager; + private SplitsStorage mSplitsStorage; + private ValidationMessageLogger mValidationMessageLogger; @Before public void loadSplitsFromFile() { + mFlagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); + mSplitsStorage = mock(SplitsStorage.class); + mValidationMessageLogger = mock(ValidationMessageLogger.class); + treatmentManager = initializeTreatmentManager(); if (evaluator == null) { FileHelper fileHelper = new FileHelper(); MySegmentsStorageContainer mySegmentsStorageContainer = mock(MySegmentsStorageContainer.class); @@ -165,7 +172,7 @@ public void testNonExistingSplits() { } @Test - public void testEmtpySplit() { + public void testEmptySplit() { String matchingKey = "nico_test"; String splitName = ""; List splitList = new ArrayList<>(); @@ -290,6 +297,24 @@ public void getTreatmentsWithConfigTakesValuesFromAttributesManagerIntoAccount() verify(attributesManager).getAllAttributes(); } + @Test + public void evaluationWhenNotReadyLogsCorrectMessage() { + ValidationMessageLogger validationMessageLogger = mock(ValidationMessageLogger.class); + SplitValidator splitValidator = mock(SplitValidator.class); + Evaluator evaluatorMock = mock(Evaluator.class); + ListenableEventsManager eventsManager = mock(ListenableEventsManager.class); + when(evaluatorMock.getTreatment(eq("my_key"), eq(null), eq("test_split"), anyMap())) + .thenReturn(new EvaluationResult("test", "test")); + when(splitValidator.validateName(any())).thenReturn(null); + when(splitValidator.splitNotFoundMessage(any())).thenReturn(null); + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(false); + when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(false); + createTreatmentManager("my_key", null, validationMessageLogger, splitValidator, evaluatorMock, eventsManager) + .getTreatment("test_split", null, false); + + verify(validationMessageLogger).w(eq("the SDK is not ready, results may be incorrect for feature flag test_split. Make sure to wait for SDK readiness before using this method"), any()); + } + private void assertControl(List splitList, String treatment, Map treatmentList, SplitResult splitResult, Map splitResultList) { Assert.assertNotNull(treatment); Assert.assertEquals(Treatments.CONTROL, treatment); @@ -315,12 +340,18 @@ private void assertControl(List splitList, String treatment, Map splitsMap(List splits) { diff --git a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java new file mode 100644 index 000000000..88d6ffbfa --- /dev/null +++ b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java @@ -0,0 +1,490 @@ +package io.split.android.client; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.ListenableEventsManager; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.model.Method; +import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.client.validators.KeyValidator; +import io.split.android.client.validators.SplitValidator; +import io.split.android.client.validators.TreatmentManagerImpl; +import io.split.android.client.validators.ValidationMessageLoggerImpl; + +public class TreatmentManagerWithFlagSetsTest { + + @Mock + private Evaluator mEvaluator; + @Mock + private KeyValidator mKeyValidator; + @Mock + private SplitValidator mSplitValidator; + @Mock + private ImpressionListener mImpressionListener; + @Mock + private ListenableEventsManager mEventsManager; + @Mock + private AttributesManager mAttributesManager; + @Mock + private AttributesMerger mAttributesMerger; + @Mock + private TelemetryStorageProducer mTelemetryStorageProducer; + @Mock + private SplitsStorage mSplitsStorage; + + private FlagSetsFilter mFlagSetsFilter; + private TreatmentManagerImpl mTreatmentManager; + private AutoCloseable mAutoCloseable; + + @Before + public void setUp() { + mAutoCloseable = MockitoAnnotations.openMocks(this); + + mFlagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); + when(mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); + when(mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(true); + + initializeTreatmentManager(); + + when(mEvaluator.getTreatment(anyString(), anyString(), eq("test_1"), anyMap())) + .thenReturn(new EvaluationResult("result_1", "label")); + when(mEvaluator.getTreatment(anyString(), anyString(), eq("test_2"), anyMap())) + .thenReturn(new EvaluationResult("result_2", "label")); + } + + @After + public void tearDown() { + try { + mAutoCloseable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void getTreatmentsByFlagSetDestroyedDoesNotUseEvaluator() { + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, true); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetWithNoConfiguredSetsQueriesStorageAndUsesEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetWithNoConfiguredSetsInvalidSetDoesNotQueryStorageNorUseEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); + + mTreatmentManager.getTreatmentsByFlagSet("SET!", null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetWithConfiguredSetsExistingSetQueriesStorageAndUsesEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetWithConfiguredSetsNonExistingSetDoesNotQueryStorageNorUseEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); + + mTreatmentManager.getTreatmentsByFlagSet("set_2", null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + private void initializeTreatmentManager() { + mTreatmentManager = new TreatmentManagerImpl( + "matching_key", + "bucketing_key", + mEvaluator, + mKeyValidator, + mSplitValidator, + mImpressionListener, + SplitClientConfig.builder().build().labelsEnabled(), + mEventsManager, + mAttributesManager, + mAttributesMerger, + mTelemetryStorageProducer, + mFlagSetsFilter, + mSplitsStorage, new ValidationMessageLoggerImpl()); + } + + @Test + public void getTreatmentsByFlagSetReturnsCorrectFormat() { + Set mockNames = new HashSet<>(); + mockNames.add("test_1"); + mockNames.add("test_2"); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(mockNames); + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + + Map result = mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + + assertEquals(2, result.size()); + assertEquals("result_1", result.get("test_1")); + assertEquals("result_2", result.get("test_2")); + } + + @Test + public void getTreatmentsByFlagSetRecordsTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); + + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + + verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_BY_FLAG_SET), anyLong()); + } + + /// + @Test + public void getTreatmentsByFlagSetsDestroyedDoesNotUseEvaluator() { + mTreatmentManager.getTreatmentsByFlagSets(Collections.singletonList("set_1"), null, true); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetsWithNoConfiguredSetsQueriesStorageAndUsesEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))) + .thenReturn(new HashSet<>(Arrays.asList("test_1", "test_2"))); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2"))); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_2"), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetsWithNoConfiguredSetsInvalidSetDoesNotQueryStorageForInvalidSet() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "SET!"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(any(), any(), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetsWithConfiguredSetsExistingSetQueriesStorageForConfiguredSetOnlyAndUsesEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetsWithConfiguredSetsNonExistingSetDoesNotQueryStorageNorUseEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_2", "set_3"), null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsByFlagSetsReturnsCorrectFormat() { + Set mockNames = new HashSet<>(); + mockNames.add("test_1"); + mockNames.add("test_2"); + when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))).thenReturn(mockNames); + + Map result = mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + assertEquals(2, result.size()); + assertEquals("result_1", result.get("test_1")); + assertEquals("result_2", result.get("test_2")); + } + + @Test + public void getTreatmentsByFlagSetsWithDuplicatedSetDeduplicates() { + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_1"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + } + + @Test + public void getTreatmentsByFlagSetsWithNullSetListReturnsEmpty() { + Map result = mTreatmentManager.getTreatmentsByFlagSets(null, null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + assertEquals(0, result.size()); + } + + @Test + public void getTreatmentsByFlagSetsRecordsTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_BY_FLAG_SETS), anyLong()); + } + + /// + @Test + public void getTreatmentsWithConfigByFlagSetDestroyedDoesNotUseEvaluator() { + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, true); + + verify(mSplitsStorage).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWithNoConfiguredSetsQueriesStorageAndUsesEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(Collections.singleton("test_1")); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWithNoConfiguredSetsInvalidSetDoesNotQueryStorageNorUseEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("SET!", null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWithConfiguredSetsExistingSetQueriesStorageAndUsesEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetWithConfiguredSetsNonExistingSetDoesNotQueryStorageNorUseEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_2", null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetReturnsCorrectFormat() { + Set mockNames = new HashSet<>(); + mockNames.add("test_1"); + mockNames.add("test_2"); + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(mockNames); + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + + assertEquals(2, result.size()); + assertEquals("result_1", result.get("test_1").treatment()); + assertEquals("result_2", result.get("test_2").treatment()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetRecordsTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + + verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET), anyLong()); + } + + /// + @Test + public void getTreatmentsWithConfigByFlagSetsDestroyedDoesNotUseEvaluator() { + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set_1"), null, true); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithNoConfiguredSetsQueriesStorageAndUsesEvaluator() { + when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))) + .thenReturn(new HashSet<>(Arrays.asList("test_1", "test_2"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2"))); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_2"), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithNoConfiguredSetsInvalidSetDoesNotQueryStorageForInvalidSet() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "SET!"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(any(), any(), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithConfiguredSetsExistingSetQueriesStorageForConfiguredSetOnlyAndUsesEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) + .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithConfiguredSetsNonExistingSetDoesNotQueryStorageNorUseEvaluator() { + mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); + initializeTreatmentManager(); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_2", "set_3"), null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsReturnsCorrectFormat() { + Set mockNames = new HashSet<>(); + mockNames.add("test_1"); + mockNames.add("test_2"); + when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))).thenReturn(mockNames); + + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + assertEquals(2, result.size()); + assertEquals("result_1", result.get("test_1").treatment()); + assertEquals("result_2", result.get("test_2").treatment()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithDuplicatedSetDeduplicates() { + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_1"), null, false); + + verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsWithNullSetListReturnsEmpty() { + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(null, null, false); + + verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); + verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); + assertEquals(0, result.size()); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsRecordsTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS), anyLong()); + } + + @Test + public void getTreatmentsByFlagSetExceptionIsRecordedInTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); + + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + + verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_BY_FLAG_SET)); + } + + @Test + public void getTreatmentsByFlagSetsExceptionIsRecordedInTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); + + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_BY_FLAG_SETS)); + } + + @Test + public void getTreatmentsWithConfigByFlagSetExceptionIsRecordedInTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); + + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + + verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET)); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsExceptionIsRecordedInTelemetry() { + when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); + + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + + verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)); + } +} diff --git a/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java b/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java index 7a557cfff..2088b392a 100644 --- a/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java +++ b/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java @@ -16,7 +16,10 @@ import org.mockito.MockitoAnnotations; import java.util.Collection; +import java.util.HashSet; +import io.split.android.client.FlagSetsFilter; +import io.split.android.client.FlagSetsFilterImpl; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.api.Key; @@ -50,12 +53,14 @@ public class LocalhostSplitClientContainerImplTest { private SplitClientConfig mConfig; @Mock private SplitTaskExecutor mTaskExecutor; + private FlagSetsFilter mFlagSetsFilter; private LocalhostSplitClientContainerImpl mClientContainer; @Before public void setUp() { MockitoAnnotations.openMocks(this); when(mAttributesManagerFactory.getManager(any(), any())).thenReturn(mock(AttributesManager.class)); + mFlagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); mClientContainer = getClientContainer(); } @@ -95,6 +100,15 @@ public void gettingNewClientRegistersEventManager() { @NonNull private LocalhostSplitClientContainerImpl getClientContainer() { - return new LocalhostSplitClientContainerImpl(mFactory, mConfig, mSplitsStorage, mSplitParser, mAttributesManagerFactory, mAttributesMerger, mTelemetryStorageProducer, mEventsManagerCoordinator, mTaskExecutor); + return new LocalhostSplitClientContainerImpl(mFactory, + mConfig, + mSplitsStorage, + mSplitParser, + mAttributesManagerFactory, + mAttributesMerger, + mTelemetryStorageProducer, + mEventsManagerCoordinator, + mTaskExecutor, + mFlagSetsFilter); } } diff --git a/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java b/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java index ae9b990ac..cb357f672 100644 --- a/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java +++ b/src/test/java/io/split/android/client/service/FilterSplitsInCacheTaskTest.java @@ -1,31 +1,28 @@ package io.split.android.client.service; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +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 org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Map; import io.split.android.client.SplitFilter; import io.split.android.client.dtos.Split; -import io.split.android.client.dtos.SplitChange; import io.split.android.client.service.splits.FilterSplitsInCacheTask; -import io.split.android.client.service.splits.SplitsSyncHelper; -import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.storage.splits.PersistentSplitsStorage; -import io.split.android.client.storage.splits.SplitsStorage; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class FilterSplitsInCacheTaskTest { @@ -35,16 +32,23 @@ public class FilterSplitsInCacheTaskTest { FilterSplitsInCacheTask mTask; List mFilters; + private AutoCloseable closeable; + @Before public void setup() { - MockitoAnnotations.initMocks(this); + closeable = MockitoAnnotations.openMocks(this); mFilters = new ArrayList<>(); } + @After + public void tearDown() throws Exception { + closeable.close(); + } + @Test public void changedQueryStringAndKeepNames() { List splits = new ArrayList<>(); - for(int i=0; i<5; i++) { + for (int i = 0; i < 5; i++) { Split split = new Split(); split.name = "sp" + i; splits.add(split); @@ -62,7 +66,7 @@ public void changedQueryStringAndKeepNames() { @Test public void changedQueryStringAndKeepPrefixes() { List splits = new ArrayList<>(); - for(int i=0; i<5; i++) { + for (int i = 0; i < 5; i++) { Split split = new Split(); split.name = "sp" + i + "__split"; splits.add(split); @@ -80,7 +84,7 @@ public void changedQueryStringAndKeepPrefixes() { @Test public void changedQueryStringAndKeepBoth() { List splits = new ArrayList<>(); - for(int i=0; i<5; i++) { + for (int i = 0; i < 5; i++) { Split split = new Split(); split.name = "sp" + i + "__split"; splits.add(split); @@ -102,7 +106,7 @@ public void changedQueryStringAndKeepBoth() { @Test public void noChangedQueryString() { List splits = new ArrayList<>(); - for(int i=0; i<5; i++) { + for (int i = 0; i < 5; i++) { Split split = new Split(); split.name = "sp" + i + "__split"; splits.add(split); @@ -121,7 +125,7 @@ public void noChangedQueryString() { @Test public void changedQueryStringNoSplitsToDelete() { List splits = new ArrayList<>(); - for(int i=1; i<4; i++) { + for (int i = 1; i < 4; i++) { Split split = new Split(); split.name = "sp" + i; splits.add(split); @@ -135,4 +139,54 @@ public void changedQueryStringNoSplitsToDelete() { verify(mSplitsStorage, never()).delete(any()); } -} \ No newline at end of file + + @Test + public void deleteSplitsNotInSet() { + List splits = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Split split = new Split(); + split.name = "sp" + i; + split.sets = new HashSet<>(); + + if (i % 3 == 0) { + split.sets.add("set1"); + } + splits.add(split); + } + + mFilters.add(SplitFilter.bySet(Collections.singletonList("set1"))); + when(mSplitsStorage.getFilterQueryString()).thenReturn("sets=set2"); + when(mSplitsStorage.getAll()).thenReturn(splits); + mTask = new FilterSplitsInCacheTask(mSplitsStorage, mFilters, "sets=set1"); + mTask.execute(); + + assertEquals(5, splits.size()); + verify(mSplitsStorage).delete(Arrays.asList("sp1", "sp2", "sp4")); + } + + @Test + public void changedSetsQueryNoSplitsToDelete() { + List splits = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Split split = new Split(); + split.name = "sp" + i; + split.sets = new HashSet<>(); + + if (i % 3 == 0) { + split.sets.add("set1"); + } else { + split.sets.add("set2"); + } + splits.add(split); + } + + mFilters.add(SplitFilter.bySet(Arrays.asList("set1", "set2"))); + when(mSplitsStorage.getFilterQueryString()).thenReturn("sets=set1,set2"); + when(mSplitsStorage.getAll()).thenReturn(splits); + mTask = new FilterSplitsInCacheTask(mSplitsStorage, mFilters, "sets=set1"); + mTask.execute(); + + assertEquals(5, splits.size()); + verify(mSplitsStorage, never()).delete(any()); + } +} diff --git a/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java b/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java new file mode 100644 index 000000000..ded9b3931 --- /dev/null +++ b/src/test/java/io/split/android/client/service/LoadSplitsTaskTest.java @@ -0,0 +1,123 @@ +package io.split.android.client.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.splits.LoadSplitsTask; +import io.split.android.client.storage.splits.SplitsStorage; + +public class LoadSplitsTaskTest { + + private SplitsStorage mSplitsStorage; + private LoadSplitsTask mLoadSplitsTask; + + @Before + public void setUp() { + mSplitsStorage = mock(SplitsStorage.class); + } + + @Test + public void resultIsErrorWhenQueryStringHasChanged() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn("previous"); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, "new"); + + SplitTaskExecutionInfo info = mLoadSplitsTask.execute(); + + assertEquals(SplitTaskExecutionStatus.ERROR, info.getStatus()); + assertEquals(SplitTaskType.LOAD_LOCAL_SPLITS, info.getTaskType()); + } + + @Test + public void resultIsSuccessWhenQueryStringIsSame() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn("previous"); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, "previous"); + + SplitTaskExecutionInfo info = mLoadSplitsTask.execute(); + + assertEquals(SplitTaskExecutionStatus.SUCCESS, info.getStatus()); + assertEquals(SplitTaskType.LOAD_LOCAL_SPLITS, info.getTaskType()); + } + + @Test + public void loadLocalIsCalledOnStorageWhenExecutingTask() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, null); + + mLoadSplitsTask.execute(); + + verify(mSplitsStorage).loadLocal(); + } + + @Test + public void resultIsErrorWhenTillIsNegativeOne() { + when(mSplitsStorage.getTill()).thenReturn(-1L); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, null); + + SplitTaskExecutionInfo info = mLoadSplitsTask.execute(); + + assertEquals(SplitTaskExecutionStatus.ERROR, info.getStatus()); + assertEquals(SplitTaskType.LOAD_LOCAL_SPLITS, info.getTaskType()); + } + + @Test + public void clearIsNotCalledWhenTillIsNegativeOne() { + when(mSplitsStorage.getTill()).thenReturn(-1L); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, null); + + mLoadSplitsTask.execute(); + + verify(mSplitsStorage, times(0)).clear(); + } + + @Test + public void clearIsCalledOnStorageWhenQueryStringsDiffer() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn("previous"); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, "new"); + + mLoadSplitsTask.execute(); + + verify(mSplitsStorage).clear(); + } + + @Test + public void clearIsNotCalledOnStorageWhenQueryStringsAreEquallyNull() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(null); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, null); + + mLoadSplitsTask.execute(); + + verify(mSplitsStorage, times(0)).clear(); + } + + @Test + public void clearIsNotCalledOnStorageWhenQueryStringsAreEqual() { + when(mSplitsStorage.getTill()).thenReturn(123456677L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(""); + + mLoadSplitsTask = new LoadSplitsTask(mSplitsStorage, ""); + + mLoadSplitsTask.execute(); + + verify(mSplitsStorage, times(0)).clear(); + } +} diff --git a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index e160aa2a1..668064db3 100644 --- a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -1,5 +1,6 @@ package io.split.android.client.service; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -27,6 +28,7 @@ import io.split.android.helpers.FileHelper; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; @@ -56,10 +58,11 @@ public class SplitsSyncHelperTest { private final Map mDefaultParams = new HashMap<>(); private final Map mSecondFetchParams = new HashMap<>(); + private AutoCloseable mAutoCloseable; @Before public void setup() { - MockitoAnnotations.openMocks(this); + mAutoCloseable = MockitoAnnotations.openMocks(this); mDefaultParams.clear(); mDefaultParams.put("since", -1L); mSecondFetchParams.clear(); @@ -68,6 +71,15 @@ public void setup() { loadSplitChanges(); } + @After + public void tearDown() { + try { + mAutoCloseable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Test public void correctSyncExecution() throws HttpFetcherException { // On correct execution without having clear param @@ -332,6 +344,28 @@ public void replaceTillWhenFilterHasChanged() throws HttpFetcherException { verifyNoMoreInteractions(mSplitsFetcher); } + @Test + public void returnTaskInfoToDoNotRetryWhenHttpFetcherExceptionStatusCodeIs414() throws HttpFetcherException { + when(mSplitsFetcher.execute(eq(mDefaultParams), any())) + .thenThrow(new HttpFetcherException("error", "error", 414)); + when(mSplitsStorage.getTill()).thenReturn(-1L); + + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + + assertEquals(true, result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); + } + + @Test + public void doNotRetryFlagIsNullWhenFetcherExceptionStatusCodeIsNot414() throws HttpFetcherException { + when(mSplitsFetcher.execute(eq(mDefaultParams), any())) + .thenThrow(new HttpFetcherException("error", "error", 500)); + when(mSplitsStorage.getTill()).thenReturn(-1L); + + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + + assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); + } + private void loadSplitChanges() { if (mSplitChange == null) { FileHelper fileHelper = new FileHelper(); diff --git a/src/test/java/io/split/android/client/service/SynchronizerTest.java b/src/test/java/io/split/android/client/service/SynchronizerTest.java index 571fd4dba..088288d08 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -176,7 +176,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M when(mMySegmentsTaskFactory.createMySegmentsSyncTask(anyBoolean())).thenReturn(Mockito.mock(MySegmentsSyncTask.class)); when(mTaskFactory.createImpressionsRecorderTask()).thenReturn(Mockito.mock(ImpressionsRecorderTask.class)); when(mTaskFactory.createEventsRecorderTask()).thenReturn(Mockito.mock(EventsRecorderTask.class)); - when(mTaskFactory.createLoadSplitsTask()).thenReturn(Mockito.mock(LoadSplitsTask.class)); + when(mTaskFactory.createLoadSplitsTask(any())).thenReturn(Mockito.mock(LoadSplitsTask.class)); when(mTaskFactory.createFilterSplitsInCacheTask()).thenReturn(Mockito.mock(FilterSplitsInCacheTask.class)); when(mTaskFactory.createImpressionsCountRecorderTask()).thenReturn(Mockito.mock(ImpressionsCountRecorderTask.class)); when(mTaskFactory.createSaveImpressionsCountTask(any())).thenReturn(Mockito.mock(SaveImpressionsCountTask.class)); @@ -524,12 +524,12 @@ public void loadLocalData() { ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer("", mMySegmentsSynchronizer); - mSynchronizer.loadSplitsFromCache(); + mSynchronizer.loadAndSynchronizeSplits(); mSynchronizer.loadMySegmentsFromCache(); mSynchronizer.loadAttributesFromCache(); + verify(mFeatureFlagsSynchronizer).loadAndSynchronize(); verify(mMySegmentsSynchronizerRegistry).loadMySegmentsFromCache(); verify(mAttributesSynchronizerRegistry).loadAttributesFromCache(); - verify(mFeatureFlagsSynchronizer).loadFromCache(); } @Test @@ -668,7 +668,7 @@ public void beingNotifiedOfSplitsSyncTaskTriggersSplitsLoad() { setup(SplitClientConfig.builder().persistentAttributesEnabled(false).build()); LoadSplitsTask task = mock(LoadSplitsTask.class); - when(mTaskFactory.createLoadSplitsTask()).thenReturn(task); + when(mTaskFactory.createLoadSplitsTask(any())).thenReturn(task); mSynchronizer.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); diff --git a/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java b/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java new file mode 100644 index 000000000..66ca13210 --- /dev/null +++ b/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java @@ -0,0 +1,304 @@ +package io.split.android.client.service.splits; + +import androidx.annotation.Nullable; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.FlagSetsFilterImpl; +import io.split.android.client.SplitFilter; +import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.Status; +import io.split.android.client.storage.splits.ProcessedSplitChange; + +public class SplitChangeProcessorTest { + + SplitChangeProcessor mProcessor; + + @Before + public void setup() { + mProcessor = new SplitChangeProcessor((SplitFilter) null, null); + } + + @Test + public void process() { + List activeSplits = createSplits(1, 10, Status.ACTIVE); + List archivedSplits = createSplits(100, 5, Status.ARCHIVED); + SplitChange change = new SplitChange(); + change.splits = activeSplits; + change.splits.addAll(archivedSplits); + + ProcessedSplitChange result = mProcessor.process(change); + + Assert.assertEquals(5, result.getArchivedSplits().size()); + Assert.assertEquals(10, result.getActiveSplits().size()); + } + + @Test + public void processNoArchived() { + List activeSplits = createSplits(1, 10, Status.ACTIVE); + SplitChange change = new SplitChange(); + change.splits = activeSplits; + + ProcessedSplitChange result = mProcessor.process(change); + + Assert.assertEquals(0, result.getArchivedSplits().size()); + Assert.assertEquals(10, result.getActiveSplits().size()); + } + + @Test + public void processNoActive() { + List archivedSplits = createSplits(100, 5, Status.ARCHIVED); + SplitChange change = new SplitChange(); + change.splits = archivedSplits; + + ProcessedSplitChange result = mProcessor.process(change); + + Assert.assertEquals(5, result.getArchivedSplits().size()); + Assert.assertEquals(0, result.getActiveSplits().size()); + } + + @Test + public void processNullSplits() { + SplitChange change = new SplitChange(); + + ProcessedSplitChange result = mProcessor.process(change); + + Assert.assertEquals(0, result.getArchivedSplits().size()); + Assert.assertEquals(0, result.getActiveSplits().size()); + } + + @Test + public void processNullNames() { + List activeSplits = createSplits(1, 10, Status.ACTIVE); + List archivedSplits = createSplits(100, 5, Status.ARCHIVED); + + activeSplits.get(0).name = null; + archivedSplits.get(0).name = null; + SplitChange change = new SplitChange(); + change.splits = activeSplits; + change.splits.addAll(archivedSplits); + + ProcessedSplitChange result = mProcessor.process(change); + + Assert.assertEquals(4, result.getArchivedSplits().size()); + Assert.assertEquals(9, result.getActiveSplits().size()); + } + + @Test + public void processSingleActiveSplit() { + Split activeSplit = createSplits(1, 1, Status.ACTIVE).get(0); + + ProcessedSplitChange result = mProcessor.process(activeSplit, 14500); + + Assert.assertEquals(0, result.getArchivedSplits().size()); + Assert.assertEquals(1, result.getActiveSplits().size()); + Assert.assertEquals(14500, result.getChangeNumber()); + + Split split = result.getActiveSplits().get(0); + Assert.assertEquals(activeSplit.name, split.name); + Assert.assertEquals(activeSplit.status, split.status); + Assert.assertEquals(activeSplit.trafficTypeName, split.trafficTypeName); + Assert.assertEquals(activeSplit.trafficAllocation, split.trafficAllocation); + Assert.assertEquals(activeSplit.trafficAllocationSeed, split.trafficAllocationSeed); + Assert.assertEquals(activeSplit.seed, split.seed); + Assert.assertEquals(activeSplit.conditions, split.conditions); + Assert.assertEquals(activeSplit.defaultTreatment, split.defaultTreatment); + Assert.assertEquals(activeSplit.configurations, split.configurations); + Assert.assertEquals(activeSplit.algo, split.algo); + Assert.assertEquals(activeSplit.changeNumber, split.changeNumber); + Assert.assertEquals(activeSplit.killed, split.killed); + } + + @Test + public void processSingleArchivedSplit() { + Split archivedSplit = createSplits(1, 1, Status.ARCHIVED).get(0); + + ProcessedSplitChange result = mProcessor.process(archivedSplit, 14500); + + Assert.assertEquals(1, result.getArchivedSplits().size()); + Assert.assertEquals(0, result.getActiveSplits().size()); + Assert.assertEquals(14500, result.getChangeNumber()); + + Split split = result.getArchivedSplits().get(0); + Assert.assertEquals(archivedSplit.name, split.name); + Assert.assertEquals(archivedSplit.status, split.status); + Assert.assertEquals(archivedSplit.trafficTypeName, split.trafficTypeName); + Assert.assertEquals(archivedSplit.trafficAllocation, split.trafficAllocation); + Assert.assertEquals(archivedSplit.trafficAllocationSeed, split.trafficAllocationSeed); + Assert.assertEquals(archivedSplit.seed, split.seed); + Assert.assertEquals(archivedSplit.conditions, split.conditions); + Assert.assertEquals(archivedSplit.defaultTreatment, split.defaultTreatment); + Assert.assertEquals(archivedSplit.configurations, split.configurations); + Assert.assertEquals(archivedSplit.algo, split.algo); + Assert.assertEquals(archivedSplit.changeNumber, split.changeNumber); + Assert.assertEquals(archivedSplit.killed, split.killed); + } + + @Test + public void processAddingWithFlagSets() { + Set configuredSets = new HashSet<>(); + configuredSets.add("set_1"); + configuredSets.add("set_2"); + SplitFilter filter = SplitFilter.bySet(new ArrayList<>(configuredSets)); + mProcessor = new SplitChangeProcessor(filter, new FlagSetsFilterImpl(configuredSets)); + + Split split1 = newSplit("split_1", Status.ACTIVE, new HashSet<>(Arrays.asList("set_3", "set_1"))); + Split split2 = newSplit("split_2", Status.ACTIVE, Collections.singleton("set_2")); + Split split3 = newSplit("split_3", Status.ACTIVE, new HashSet<>(Collections.singletonList("set_3"))); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Arrays.asList(split1, split2, split3); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(2, result.getActiveSplits().size()); + Assert.assertEquals(1, result.getArchivedSplits().size()); + } + + @Test + public void featureFlagWithNoSetsIsArchivedWhenProcessingWithFlagSets() { + Set configuredSets = new HashSet<>(); + configuredSets.add("set_1"); + configuredSets.add("set_2"); + SplitFilter filter = SplitFilter.bySet(new ArrayList<>(configuredSets)); + + mProcessor = new SplitChangeProcessor(filter, new FlagSetsFilterImpl(configuredSets)); + + Split split1 = newSplit("split_1", Status.ACTIVE, null); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Collections.singletonList(split1); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(0, result.getActiveSplits().size()); + Assert.assertEquals(1, result.getArchivedSplits().size()); + } + + @Test + public void featureFlagsAreFilteredByNameWhenThereIsSplitFilterByName() { + SplitFilter filter = SplitFilter.byName(Arrays.asList("split_1", "split_2")); + + mProcessor = new SplitChangeProcessor(filter, null); + + Split split1 = newSplit("split_1", Status.ACTIVE); + Split split2 = newSplit("split_2", Status.ARCHIVED); + Split split3 = newSplit("split_3", Status.ACTIVE); + Split split4 = newSplit("split_4", Status.ARCHIVED); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Arrays.asList(split1, split2, split3, split4); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(1, result.getActiveSplits().size()); + Assert.assertEquals(1, result.getArchivedSplits().size()); + } + + @Test + public void creatingWithNullFilterProcessesEverything() { + Map filterMap = null; + mProcessor = new SplitChangeProcessor(filterMap, null); + + Split split1 = newSplit("split_1", Status.ACTIVE); + Split split2 = newSplit("split_2", Status.ARCHIVED); + Split split3 = newSplit("split_3", Status.ACTIVE); + Split split4 = newSplit("split_4", Status.ARCHIVED); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Arrays.asList(split1, split2, split3, split4); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(2, result.getActiveSplits().size()); + Assert.assertEquals(2, result.getArchivedSplits().size()); + } + + @Test + public void creatingWithFilterWithEmptyConfiguredValuesProcessesEverything() { + Map filterMap = Collections.singletonMap(SplitFilter.Type.BY_SET, SplitFilter.bySet(Collections.emptyList())); + + mProcessor = new SplitChangeProcessor(filterMap, new FlagSetsFilterImpl(Collections.emptySet())); + + Split split1 = newSplit("split_1", Status.ACTIVE); + Split split2 = newSplit("split_2", Status.ARCHIVED); + Split split3 = newSplit("split_3", Status.ACTIVE); + Split split4 = newSplit("split_4", Status.ARCHIVED); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Arrays.asList(split1, split2, split3, split4); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(2, result.getActiveSplits().size()); + Assert.assertEquals(2, result.getArchivedSplits().size()); + } + + @Test + public void nonConfiguredSetsAreNotRemovedFromSplit() { + Set configuredSets = new HashSet<>(); + configuredSets.add("set_1"); + configuredSets.add("set_2"); + SplitFilter filter = SplitFilter.bySet(new ArrayList<>(configuredSets)); + mProcessor = new SplitChangeProcessor(filter, new FlagSetsFilterImpl(configuredSets)); + + Split split1 = newSplit("split_1", Status.ACTIVE, new HashSet<>(Arrays.asList("set_1", "set_3"))); + Split split2 = newSplit("split_2", Status.ACTIVE, new HashSet<>(Arrays.asList("set_2", "set_asd"))); + Split split3 = newSplit("split_3", Status.ACTIVE, new HashSet<>(Collections.singletonList("set_3"))); + int initialSplit1Sets = split1.sets.size(); + int initialSplit2Sets = split2.sets.size(); + + SplitChange splitChange = new SplitChange(); + splitChange.splits = Arrays.asList(split1, split2, split3); + + ProcessedSplitChange result = mProcessor.process(splitChange); + + Assert.assertEquals(2, result.getActiveSplits().size()); + Assert.assertEquals(1, result.getArchivedSplits().size()); + + Split processedSplit1 = result.getActiveSplits().get(0); + Assert.assertEquals(split1.name, processedSplit1.name); + Assert.assertEquals(2, initialSplit1Sets); + Assert.assertEquals(2, processedSplit1.sets.size()); + Assert.assertTrue(processedSplit1.sets.contains("set_1")); + Assert.assertTrue(processedSplit1.sets.contains("set_3")); + + Split processedSplit2 = result.getActiveSplits().get(1); + Assert.assertEquals(split2.name, processedSplit2.name); + Assert.assertEquals(2, initialSplit2Sets); + Assert.assertEquals(2, processedSplit2.sets.size()); + Assert.assertTrue(processedSplit2.sets.contains("set_2")); + } + + private List createSplits(int from, int count, Status status) { + List splits = new ArrayList<>(); + for (int i = from; i < count + from; i++) { + Split split = newSplit("split_" + i, status); + splits.add(split); + } + return splits; + } + + private Split newSplit(String name, Status status) { + return newSplit(name, status, null); + } + + private Split newSplit(String name, Status status, @Nullable Set sets) { + Split split = new Split(); + split.name = name; + split.status = status; + split.sets = sets; + return split; + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java b/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java index eed9d7635..d0d07fbaf 100644 --- a/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java @@ -1,23 +1,28 @@ package io.split.android.client.service.sseclient.sseclient; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.sseclient.BackoffCounter; @@ -31,13 +36,23 @@ public class RetryBackoffCounterTimerTest { @Mock private SplitTask mockTask; private RetryBackoffCounterTimer counterTimer; + private AutoCloseable mAutoCloseable; @Before public void setUp() { - MockitoAnnotations.openMocks(this); + mAutoCloseable = MockitoAnnotations.openMocks(this); counterTimer = new RetryBackoffCounterTimer(taskExecutor, backoffCounter); } + @After + public void tearDown() { + try { + mAutoCloseable.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Test public void stopCallsStopInTaskExecutorWhenTaskIsNotNull() { when(taskExecutor.schedule(mockTask, @@ -115,4 +130,43 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { assertTrue(await); verify(taskExecutor, times(2)).schedule(mockTask, 0L, counterTimer); } + + @Test + public void nonRetryableErrorTaskIsNotRetried() { + counterTimer = new RetryBackoffCounterTimer(taskExecutor, backoffCounter); + + SplitTaskExecutionListener mockListener = mock(SplitTaskExecutionListener.class); + when(taskExecutor.schedule(mockTask, + 0L, + counterTimer)).then(invocation -> { + counterTimer.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC, Collections.singletonMap("DO_NOT_RETRY", true))); + return "100"; + }); + + counterTimer.setTask(mockTask, mockListener); + + counterTimer.start(); + + verify(taskExecutor).schedule(mockTask, 0L, counterTimer); + } + + @Test + public void nonRetryableErrorTaskNotifiesListenerWithErrorStatus() { + counterTimer = new RetryBackoffCounterTimer(taskExecutor, backoffCounter); + + SplitTaskExecutionListener mockListener = mock(SplitTaskExecutionListener.class); + when(taskExecutor.schedule(mockTask, + 0L, + counterTimer)).then(invocation -> { + counterTimer.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC, Collections.singletonMap("DO_NOT_RETRY", true))); + return "100"; + }); + + counterTimer.setTask(mockTask, mockListener); + + counterTimer.start(); + + verify(mockListener).taskExecuted(argThat(taskInfo -> taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR && + taskInfo.getTaskType() == SplitTaskType.SPLITS_SYNC)); + } } diff --git a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java index e4d3b2217..91f0800a8 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java @@ -64,7 +64,7 @@ public void setUp() { mFeatureFlagsSynchronizer = new FeatureFlagsSynchronizerImpl(mConfig, mTaskExecutor, mSingleThreadTaskExecutor, mTaskFactory, - mEventsManager, mRetryBackoffCounterFactory, mPushManagerEventBroadcaster); + mEventsManager, mRetryBackoffCounterFactory, mPushManagerEventBroadcaster, ""); } @Test @@ -78,25 +78,11 @@ public void synchronizeSplitsWithSince() { verify(mRetryTimerSplitsUpdate).start(); } - @Test - public void loadLocalData() { - LoadSplitsTask mockTask = mock(LoadSplitsTask.class); - when(mockTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); - when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockTask); - when(mRetryBackoffCounterFactory.create(any(), anyInt())) - .thenReturn(mRetryTimerSplitsSync) - .thenReturn(mRetryTimerSplitsUpdate); - - mFeatureFlagsSynchronizer.loadFromCache(); - - verify(mTaskExecutor).submit(eq(mockTask), argThat(Objects::nonNull)); - } - @Test public void loadAndSynchronizeSplits() { LoadSplitsTask mockLoadTask = mock(LoadSplitsTask.class); when(mockLoadTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); - when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockLoadTask); + when(mTaskFactory.createLoadSplitsTask(any())).thenReturn(mockLoadTask); FilterSplitsInCacheTask mockFilterTask = mock(FilterSplitsInCacheTask.class); when(mockFilterTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE)); @@ -113,7 +99,7 @@ public void loadAndSynchronizeSplits() { mFeatureFlagsSynchronizer.loadAndSynchronize(); verify(mTaskFactory).createFilterSplitsInCacheTask(); - verify(mTaskFactory).createLoadSplitsTask(); + verify(mTaskFactory).createLoadSplitsTask(any()); ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); verify(mTaskExecutor).executeSerially(argument.capture()); diff --git a/src/test/java/io/split/android/client/service/synchronizer/WorkManagerWrapperTest.java b/src/test/java/io/split/android/client/service/synchronizer/WorkManagerWrapperTest.java index 67d262f33..edfbd899e 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/WorkManagerWrapperTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/WorkManagerWrapperTest.java @@ -19,11 +19,15 @@ import org.mockito.MockitoAnnotations; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.concurrent.TimeUnit; import io.split.android.client.ServiceEndpoints; import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFilter; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.workmanager.EventsRecorderWorker; import io.split.android.client.service.workmanager.ImpressionsRecorderWorker; @@ -68,7 +72,8 @@ public void setUp() throws Exception { mWorkManager, splitClientConfig, "api_key", - "test_database_name" + "test_database_name", + SplitFilter.bySet(Arrays.asList("set_1", "set_2")) ); } @@ -90,6 +95,8 @@ public void scheduleWorkSchedulesSplitsJob() { .putLong("splitCacheExpiration", 864000) .putString("endpoint", "https://test.split.io/api") .putBoolean("shouldRecordTelemetry", true) + .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) + .putString("configuredFilterType", SplitFilter.Type.BY_SET.queryStringField()) .build(); PeriodicWorkRequest expectedRequest = new PeriodicWorkRequest diff --git a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java index ff4e6c69f..35922f2d7 100644 --- a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java +++ b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java @@ -103,7 +103,8 @@ public void setUp() { mTaskFactory, mEventsManager, mRetryBackoffCounterFactory, - mPushManagerEventBroadcaster), + mPushManagerEventBroadcaster, + ""), mSplitStorageContainer.getEventsStorage()); } diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java b/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java index 183ea2f42..f15584ed5 100644 --- a/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java +++ b/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java @@ -24,7 +24,7 @@ public void setUp() { @Test public void jsonIsBuiltAsExpected() { - final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"rR\":{\"sp\":4000,\"ms\":5000,\"im\":3000,\"ev\":2000,\"te\":1000},\"uO\":{\"s\":true,\"e\":true,\"a\":true,\"st\":true,\"t\":true},\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":1,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; + final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"rR\":{\"sp\":4000,\"ms\":5000,\"im\":3000,\"ev\":2000,\"te\":1000},\"uO\":{\"s\":true,\"e\":true,\"a\":true,\"st\":true,\"t\":true},\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":1,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"],\"fsT\":4,\"fsI\":2}"; final String serializedConfig = telemetryConfigBodySerializer.serialize(buildMockConfig()); assertEquals(expectedJson, serializedConfig); @@ -33,7 +33,7 @@ public void jsonIsBuiltAsExpected() { @Test public void nullValuesAreIgnoredForJson() { - final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":0,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; + final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":0,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"],\"fsT\":0,\"fsI\":0}"; final String serializedConfig = telemetryConfigBodySerializer.serialize(buildMockConfigWithNulls()); assertEquals(expectedJson, serializedConfig); @@ -71,6 +71,8 @@ private Config buildMockConfig() { config.setUserConsent(1); config.setTags(Arrays.asList("tag1", "tag2")); config.setIntegrations(Arrays.asList("integration1", "integration2")); + config.setFlagSetsTotal(4); + config.setFlagSetsInvalid(2); return config; } diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java b/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java index a0dc2bdbe..1a6ec1cd4 100644 --- a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java +++ b/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java @@ -30,7 +30,7 @@ public void setUp() { public void jsonIsBuiltAsExpected() { String serializedStats = telemetryStatsBodySerializer.serialize(getMockStats()); - assertEquals("{\"lS\":{\"sp\":1000,\"ms\":2000,\"im\":3000,\"ic\":4000,\"ev\":5000,\"te\":6000,\"to\":7000},\"mL\":{\"t\":[0,0,2,0],\"ts\":[0,0,3,0],\"tc\":[0,0,5,0],\"tcs\":[0,0,4,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tr\":1},\"hE\":{},\"hL\":{\"sp\":[0,0,3,0],\"ms\":[0,0,5,0],\"im\":[0,0,1,0],\"ic\":[0,0,4,0],\"ev\":[0,0,2,0],\"te\":[1,0,0,0],\"to\":[0,0,6,0]},\"tR\":4,\"aR\":5,\"iQ\":2,\"iDe\":5,\"iDr\":4,\"spC\":456,\"seC\":4,\"skC\":1,\"sL\":2000,\"eQ\":4,\"eD\":2,\"sE\":[{\"e\":0,\"t\":5000},{\"e\":20,\"d\":4,\"t\":2000}],\"t\":[\"tag1\",\"tag2\"],\"ufs\":{\"sp\":4,\"ms\":8}}", serializedStats); + assertEquals("{\"lS\":{\"sp\":1000,\"ms\":2000,\"im\":3000,\"ic\":4000,\"ev\":5000,\"te\":6000,\"to\":7000},\"mL\":{\"t\":[0,0,2,0],\"ts\":[0,0,3,0],\"tc\":[0,0,5,0],\"tcs\":[0,0,4,0],\"tf\":[1,0,0,0],\"tfs\":[2,0,0,0],\"tcf\":[3,0,0,0],\"tcfs\":[4,0,0,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tf\":10,\"tfs\":20,\"tcf\":30,\"tcfs\":40,\"tr\":1},\"hE\":{},\"hL\":{\"sp\":[0,0,3,0],\"ms\":[0,0,5,0],\"im\":[0,0,1,0],\"ic\":[0,0,4,0],\"ev\":[0,0,2,0],\"te\":[1,0,0,0],\"to\":[0,0,6,0]},\"tR\":4,\"aR\":5,\"iQ\":2,\"iDe\":5,\"iDr\":4,\"spC\":456,\"seC\":4,\"skC\":1,\"sL\":2000,\"eQ\":4,\"eD\":2,\"sE\":[{\"e\":0,\"t\":5000},{\"e\":20,\"d\":4,\"t\":2000}],\"t\":[\"tag1\",\"tag2\"],\"ufs\":{\"sp\":4,\"ms\":8}}", serializedStats); } private Stats getMockStats() { @@ -54,6 +54,10 @@ private Stats getMockStats() { methodLatencies.setTreatments(Arrays.asList(0L, 0L, 3L, 0L)); methodLatencies.setTreatmentsWithConfig(Arrays.asList(0L, 0L, 4L, 0L)); methodLatencies.setTreatmentWithConfig(Arrays.asList(0L, 0L, 5L, 0L)); + methodLatencies.setTreatmentsByFlagSet(Arrays.asList(1L, 0L, 0L, 0L)); + methodLatencies.setTreatmentsByFlagSets(Arrays.asList(2L, 0L, 0L, 0L)); + methodLatencies.setTreatmentsWithConfigByFlagSet(Arrays.asList(3L, 0L, 0L, 0L)); + methodLatencies.setTreatmentsWithConfigByFlagSets(Arrays.asList(4L, 0L, 0L, 0L)); MethodExceptions methodExceptions = new MethodExceptions(); methodExceptions.setTrack(1); @@ -61,6 +65,10 @@ private Stats getMockStats() { methodExceptions.setTreatments(3); methodExceptions.setTreatmentsWithConfig(4); methodExceptions.setTreatmentWithConfig(5); + methodExceptions.setTreatmentsByFlagSet(10); + methodExceptions.setTreatmentsByFlagSets(20); + methodExceptions.setTreatmentsWithConfigByFlagSet(30); + methodExceptions.setTreatmentsWithConfigByFlagSets(40); stats.setHttpLatencies(httpLatencies); stats.setAuthRejections(5); diff --git a/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java b/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java index 97c6adbe5..4227802af 100644 --- a/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java +++ b/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java @@ -50,6 +50,10 @@ public void popExceptionsReturnsCorrectlyBuiltMethodExceptions() { telemetryStorage.recordException(Method.TREATMENTS); telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG); telemetryStorage.recordException(Method.TREATMENT_WITH_CONFIG); + telemetryStorage.recordException(Method.TREATMENTS_BY_FLAG_SET); + telemetryStorage.recordException(Method.TREATMENTS_BY_FLAG_SETS); + telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET); + telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); MethodExceptions methodExceptions = telemetryStorage.popExceptions(); @@ -58,6 +62,10 @@ public void popExceptionsReturnsCorrectlyBuiltMethodExceptions() { assertEquals(1, methodExceptions.getTreatments()); assertEquals(1, methodExceptions.getTreatmentsWithConfig()); assertEquals(1, methodExceptions.getTreatmentWithConfig()); + assertEquals(1, methodExceptions.getTreatmentsByFlagSet()); + assertEquals(1, methodExceptions.getTreatmentsByFlagSets()); + assertEquals(1, methodExceptions.getTreatmentsWithConfigByFlagSet()); + assertEquals(1, methodExceptions.getTreatmentsWithConfigByFlagSets()); } @Test @@ -68,6 +76,10 @@ public void popExceptionsEmptiesCounters() { telemetryStorage.recordException(Method.TREATMENTS); telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG); telemetryStorage.recordException(Method.TREATMENT_WITH_CONFIG); + telemetryStorage.recordException(Method.TREATMENTS_BY_FLAG_SET); + telemetryStorage.recordException(Method.TREATMENTS_BY_FLAG_SETS); + telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET); + telemetryStorage.recordException(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); telemetryStorage.popExceptions(); @@ -78,6 +90,10 @@ public void popExceptionsEmptiesCounters() { assertEquals(0, secondPop.getTreatments()); assertEquals(0, secondPop.getTreatmentsWithConfig()); assertEquals(0, secondPop.getTreatmentWithConfig()); + assertEquals(0, secondPop.getTreatmentsByFlagSet()); + assertEquals(0, secondPop.getTreatmentsByFlagSets()); + assertEquals(0, secondPop.getTreatmentsWithConfigByFlagSet()); + assertEquals(0, secondPop.getTreatmentsWithConfigByFlagSets()); } @Test @@ -89,6 +105,10 @@ public void popLatenciesReturnsCorrectlyBuiltObject() { telemetryStorage.recordLatency(Method.TREATMENTS, 200); telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG, 10); telemetryStorage.recordLatency(Method.TREATMENT_WITH_CONFIG, 2000); + telemetryStorage.recordLatency(Method.TREATMENTS_BY_FLAG_SET, 15); + telemetryStorage.recordLatency(Method.TREATMENTS_BY_FLAG_SETS, 14); + telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, 15); + telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, 14); MethodLatencies methodLatencies = telemetryStorage.popLatencies(); @@ -97,12 +117,20 @@ public void popLatenciesReturnsCorrectlyBuiltObject() { assertFalse(methodLatencies.getTreatments().stream().allMatch(l -> l == 0)); assertFalse(methodLatencies.getTreatmentsWithConfig().stream().allMatch(l -> l == 0)); assertFalse(methodLatencies.getTreatmentWithConfig().stream().allMatch(l -> l == 0)); + assertFalse(methodLatencies.getTreatmentsByFlagSet().stream().allMatch(l -> l == 0)); + assertFalse(methodLatencies.getTreatmentsByFlagSets().stream().allMatch(l -> l == 0)); + assertFalse(methodLatencies.getTreatmentsWithConfigByFlagSet().stream().allMatch(l -> l == 0)); + assertFalse(methodLatencies.getTreatmentsWithConfigByFlagSets().stream().allMatch(l -> l == 0)); assertEquals(1, (long) methodLatencies.getTreatment().get(15)); assertEquals(1, (long) methodLatencies.getTreatments().get(14)); assertEquals(1, (long) methodLatencies.getTreatmentWithConfig().get(19)); assertEquals(1, (long) methodLatencies.getTreatmentsWithConfig().get(6)); assertEquals(1, (long) methodLatencies.getTrack().get(14)); + assertEquals(1, (long) methodLatencies.getTreatmentsByFlagSet().get(7)); + assertEquals(1, (long) methodLatencies.getTreatmentsByFlagSets().get(7)); + assertEquals(1, (long) methodLatencies.getTreatmentsWithConfigByFlagSet().get(7)); + assertEquals(1, (long) methodLatencies.getTreatmentsWithConfigByFlagSets().get(7)); } @Test @@ -113,6 +141,10 @@ public void secondLatenciesPopHasArraysSetIn0() { telemetryStorage.recordLatency(Method.TREATMENTS, 200); telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG, 10); telemetryStorage.recordLatency(Method.TREATMENT_WITH_CONFIG, 2000); + telemetryStorage.recordLatency(Method.TREATMENTS_BY_FLAG_SET, 15); + telemetryStorage.recordLatency(Method.TREATMENTS_BY_FLAG_SETS, 14); + telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, 15); + telemetryStorage.recordLatency(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, 14); telemetryStorage.popLatencies(); @@ -123,6 +155,10 @@ public void secondLatenciesPopHasArraysSetIn0() { assertTrue(methodLatencies.getTreatments().stream().allMatch(l -> l == 0)); assertTrue(methodLatencies.getTreatmentsWithConfig().stream().allMatch(l -> l == 0)); assertTrue(methodLatencies.getTreatmentWithConfig().stream().allMatch(l -> l == 0)); + assertTrue(methodLatencies.getTreatmentsByFlagSet().stream().allMatch(l -> l == 0)); + assertTrue(methodLatencies.getTreatmentsByFlagSets().stream().allMatch(l -> l == 0)); + assertTrue(methodLatencies.getTreatmentsWithConfigByFlagSet().stream().allMatch(l -> l == 0)); + assertTrue(methodLatencies.getTreatmentsWithConfigByFlagSets().stream().allMatch(l -> l == 0)); } @Test diff --git a/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java b/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java index 6047c2c20..8e4b773f6 100644 --- a/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java +++ b/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java @@ -60,7 +60,7 @@ public void close() { } }) .build(); - mTelemetryConfigProvider = new TelemetryConfigProviderImpl(mTelemetryStorageConsumer, mSplitClientConfig); + mTelemetryConfigProvider = new TelemetryConfigProviderImpl(mTelemetryStorageConsumer, mSplitClientConfig, 4, 2); Config configTelemetry = mTelemetryConfigProvider.getConfigTelemetry(); @@ -81,5 +81,7 @@ public void close() { assertTrue(configTelemetry.getUrlOverrides().isEvents()); assertTrue(configTelemetry.getUrlOverrides().isAuth()); assertTrue(configTelemetry.getUrlOverrides().isStream()); + assertEquals(6, configTelemetry.getFlagSetsTotal()); + assertEquals(2, configTelemetry.getFlagSetsInvalid()); } } diff --git a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java index ad91b25a0..e20344b3e 100644 --- a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java +++ b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java @@ -2,8 +2,10 @@ import static org.mockito.Mockito.mock; -import io.split.android.client.EvaluatorImpl; +import java.util.Collections; + import io.split.android.client.EventsTracker; +import io.split.android.client.FlagSetsFilterImpl; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitClientImpl; import io.split.android.client.SplitFactory; @@ -13,7 +15,6 @@ import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.impressions.ImpressionListener; -import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.shared.SplitClientContainer; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; @@ -27,10 +28,7 @@ import io.split.android.engine.experiments.SplitParser; import io.split.android.fake.SplitTaskExecutorStub; -/** - * Created by fernandomartin on 2/17/18. - */ - +@Deprecated public class SplitClientImplFactory { public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { @@ -40,8 +38,8 @@ public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { TelemetryStorage telemetryStorage = mock(TelemetryStorage.class); TreatmentManagerFactory treatmentManagerFactory = new TreatmentManagerFactoryImpl( new KeyValidatorImpl(), new SplitValidatorImpl(), new ImpressionListener.NoopImpressionListener(), - false, new AttributesMergerImpl(), telemetryStorage, new EvaluatorImpl(splitsStorage, splitParser) - ); + false, new AttributesMergerImpl(), telemetryStorage, splitParser, + new FlagSetsFilterImpl(Collections.emptySet()), splitsStorage); AttributesManager attributesManager = mock(AttributesManager.class); SplitClientImpl c = new SplitClientImpl( diff --git a/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java b/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java new file mode 100644 index 000000000..075317f1d --- /dev/null +++ b/src/test/java/io/split/android/client/validators/FlagSetsValidatorImplTest.java @@ -0,0 +1,104 @@ +package io.split.android.client.validators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class FlagSetsValidatorImplTest { + + private final FlagSetsValidatorImpl mValidator = new FlagSetsValidatorImpl(); + + @Test + public void nullInputReturnsEmptyList() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", null); + assertTrue(result.getValues().isEmpty()); + assertEquals(0, result.getInvalidValueCount()); + } + + @Test + public void emptyInputReturnsEmptyList() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Collections.emptyList()); + assertTrue(result.getValues().isEmpty()); + assertEquals(0, result.getInvalidValueCount()); + } + + @Test + public void duplicatedInputValuesAreRemoved() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1", "set1")); + assertEquals(1, result.getValues().size()); + assertTrue(result.getValues().contains("set1")); + assertEquals(1, result.getInvalidValueCount()); + } + + @Test + public void valuesAreSortedAlphanumerically() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set2", "set1", "set_1", "1set")); + assertEquals(4, result.getValues().size()); + assertEquals("1set", result.getValues().get(0)); + assertEquals("set1", result.getValues().get(1)); + assertEquals("set2", result.getValues().get(2)); + assertEquals("set_1", result.getValues().get(3)); + assertEquals(0, result.getInvalidValueCount()); + } + + @Test + public void invalidValuesAreRemoved() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1", "set2", "set_1", "set-1", "set 1", "set 2")); + assertEquals(3, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals("set2", result.getValues().get(1)); + assertEquals("set_1", result.getValues().get(2)); + assertEquals(3, result.getInvalidValueCount()); + } + + @Test + public void setWithMoreThan50CharsIsRemoved() { + String longSet = "abcdfghijklmnopqrstuvwxyz1234567890abcdfghijklmnopq"; + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1", longSet)); + assertEquals(51, longSet.length()); + assertEquals(1, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals(1, result.getInvalidValueCount()); + } + + @Test + public void setWithLessThanOneCharIsOrEmptyRemoved() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1", "", " ")); + assertEquals(1, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals(2, result.getInvalidValueCount()); + } + + @Test + public void nullSetIsRemoved() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1", null)); + assertEquals(1, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals(1, result.getInvalidValueCount()); + } + + @Test + public void setWithExtraWhitespaceIsTrimmed() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("set1 ", " set2\r", "set3 ", "set 4\n")); + assertEquals(3, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals("set2", result.getValues().get(1)); + assertEquals("set3", result.getValues().get(2)); + assertEquals(1, result.getInvalidValueCount()); + } + + @Test + public void setsAreLowercase() { + SplitFilterValidator.ValidationResult result = mValidator.cleanup("method", Arrays.asList("SET1", "Set2", "SET_3")); + assertEquals(3, result.getValues().size()); + assertEquals("set1", result.getValues().get(0)); + assertEquals("set2", result.getValues().get(1)); + assertEquals("set_3", result.getValues().get(2)); + assertEquals(0, result.getInvalidValueCount()); + } +} diff --git a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java index eb4f90da2..18fdf9fe8 100644 --- a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -351,6 +352,7 @@ private Split makeSplit(String name, List conditions, long changeNumb split.changeNumber = changeNumber; split.algo = 1; split.configurations = configurations; + split.sets = Collections.emptySet(); return split; } diff --git a/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java b/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java deleted file mode 100644 index 624cf214b..000000000 --- a/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package io.split.android.engine.splits; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import io.split.android.client.dtos.Split; -import io.split.android.client.dtos.SplitChange; -import io.split.android.client.dtos.Status; -import io.split.android.client.storage.splits.ProcessedSplitChange; -import io.split.android.client.service.splits.SplitChangeProcessor; - -public class SplitChangeProcessorTest { - - SplitChangeProcessor mProcessor; - - @Before - public void setup() { - mProcessor = new SplitChangeProcessor(); - } - - @Test - public void process() { - List activeSplits = createSplits(1, 10, Status.ACTIVE); - List archivedSplits = createSplits(100, 5, Status.ARCHIVED); - SplitChange change = new SplitChange(); - change.splits = activeSplits; - change.splits.addAll(archivedSplits); - - ProcessedSplitChange result = mProcessor.process(change); - - Assert.assertEquals(5, result.getArchivedSplits().size()); - Assert.assertEquals(10, result.getActiveSplits().size()); - } - - @Test - public void processNoArchived() { - List activeSplits = createSplits(1, 10, Status.ACTIVE); - SplitChange change = new SplitChange(); - change.splits = activeSplits; - - ProcessedSplitChange result = mProcessor.process(change); - - Assert.assertEquals(0, result.getArchivedSplits().size()); - Assert.assertEquals(10, result.getActiveSplits().size()); - } - - @Test - public void processNoActive() { - List archivedSplits = createSplits(100, 5, Status.ARCHIVED); - SplitChange change = new SplitChange(); - change.splits = archivedSplits; - - ProcessedSplitChange result = mProcessor.process(change); - - Assert.assertEquals(5, result.getArchivedSplits().size()); - Assert.assertEquals(0, result.getActiveSplits().size()); - } - - @Test - public void processNullSplits() { - SplitChange change = new SplitChange(); - - ProcessedSplitChange result = mProcessor.process(change); - - Assert.assertEquals(0, result.getArchivedSplits().size()); - Assert.assertEquals(0, result.getActiveSplits().size()); - } - - @Test - public void processNullNames() { - List activeSplits = createSplits(1, 10, Status.ACTIVE); - List archivedSplits = createSplits(100, 5, Status.ARCHIVED); - - activeSplits.get(0).name = null; - archivedSplits.get(0).name = null; - SplitChange change = new SplitChange(); - change.splits = activeSplits; - change.splits.addAll(archivedSplits); - - ProcessedSplitChange result = mProcessor.process(change); - - Assert.assertEquals(4, result.getArchivedSplits().size()); - Assert.assertEquals(9, result.getActiveSplits().size()); - } - - @Test - public void processSingleActiveSplit() { - Split activeSplit = createSplits(1, 1, Status.ACTIVE).get(0); - - ProcessedSplitChange result = mProcessor.process(activeSplit, 14500); - - Assert.assertEquals(0, result.getArchivedSplits().size()); - Assert.assertEquals(1, result.getActiveSplits().size()); - Assert.assertEquals(14500, result.getChangeNumber()); - - Split split = result.getActiveSplits().get(0); - Assert.assertEquals(activeSplit.name, split.name); - Assert.assertEquals(activeSplit.status, split.status); - Assert.assertEquals(activeSplit.trafficTypeName, split.trafficTypeName); - Assert.assertEquals(activeSplit.trafficAllocation, split.trafficAllocation); - Assert.assertEquals(activeSplit.trafficAllocationSeed, split.trafficAllocationSeed); - Assert.assertEquals(activeSplit.seed, split.seed); - Assert.assertEquals(activeSplit.conditions, split.conditions); - Assert.assertEquals(activeSplit.defaultTreatment, split.defaultTreatment); - Assert.assertEquals(activeSplit.configurations, split.configurations); - Assert.assertEquals(activeSplit.algo, split.algo); - Assert.assertEquals(activeSplit.changeNumber, split.changeNumber); - Assert.assertEquals(activeSplit.killed, split.killed); - } - - @Test - public void processSingleArchivedSplit() { - Split archivedSplit = createSplits(1, 1, Status.ARCHIVED).get(0); - - ProcessedSplitChange result = mProcessor.process(archivedSplit, 14500); - - Assert.assertEquals(1, result.getArchivedSplits().size()); - Assert.assertEquals(0, result.getActiveSplits().size()); - Assert.assertEquals(14500, result.getChangeNumber()); - - Split split = result.getArchivedSplits().get(0); - Assert.assertEquals(archivedSplit.name, split.name); - Assert.assertEquals(archivedSplit.status, split.status); - Assert.assertEquals(archivedSplit.trafficTypeName, split.trafficTypeName); - Assert.assertEquals(archivedSplit.trafficAllocation, split.trafficAllocation); - Assert.assertEquals(archivedSplit.trafficAllocationSeed, split.trafficAllocationSeed); - Assert.assertEquals(archivedSplit.seed, split.seed); - Assert.assertEquals(archivedSplit.conditions, split.conditions); - Assert.assertEquals(archivedSplit.defaultTreatment, split.defaultTreatment); - Assert.assertEquals(archivedSplit.configurations, split.configurations); - Assert.assertEquals(archivedSplit.algo, split.algo); - Assert.assertEquals(archivedSplit.changeNumber, split.changeNumber); - Assert.assertEquals(archivedSplit.killed, split.killed); - } - - private List createSplits(int from, int count, Status status) { - List splits = new ArrayList<>(); - for(int i=from; i configurations + ) { + return createSplit(feature, + seed, + killed, + defaultTreatment, + conditions, + trafficTypeName, + changeNumber, + algo, + configurations, + Collections.emptySet()); + } + + public static Split createSplit( + String feature, + int seed, + boolean killed, + String defaultTreatment, + List conditions, + String trafficTypeName, + long changeNumber, + int algo, + Map configurations, + Set sets ) { Split split = new Split(); split.name = feature; @@ -52,11 +78,11 @@ public static Split createSplit( split.trafficTypeName = trafficTypeName; split.changeNumber = changeNumber; split.trafficAllocation = 100; - split.seed = seed; split.trafficAllocationSeed = seed; split.algo = algo; split.status = Status.ACTIVE; split.configurations = configurations; + split.sets = sets; return split; } @@ -95,7 +121,8 @@ public static ParsedSplit createParsedSplit( 100, seed, algo, - configurations + configurations, + Collections.emptySet() ); } From 3f904d1758c5bfeadefdb1514e0aeab7f98dbb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Thea?= Date: Tue, 31 Oct 2023 17:46:13 -0300 Subject: [PATCH 3/6] Update enhancements (#550) --- build.gradle | 2 +- .../sets/FlagSetsStreamingTest.java | 2 +- .../java/tests/storage/SplitsStorageTest.java | 83 +++++++++++++++---- .../localhost/LocalhostSplitsStorage.java | 3 +- .../splits/SplitInPlaceUpdateTask.java | 6 +- .../client/storage/splits/SplitsStorage.java | 3 +- .../storage/splits/SplitsStorageImpl.java | 14 +++- .../service/SplitInPlaceUpdateTaskTest.java | 31 ++++++- 8 files changed, 121 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 78ec8cda1..a5245952a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.4.0-alpha-1' + splitVersion = '3.4.0-alpha-2' } android { diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java index d61a46e06..20d3afa06 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java @@ -134,7 +134,7 @@ public void sdkWithSetsConfiguredDeletedDueToNonMatchingSets() throws IOExceptio assertTrue(firstChange); assertTrue(secondChange); assertTrue(thirdChange); - assertTrue(fourthChange); + assertFalse(fourthChange); // SDK_UPDATE should not be triggered since there were no changes to storage } @Test diff --git a/src/androidTest/java/tests/storage/SplitsStorageTest.java b/src/androidTest/java/tests/storage/SplitsStorageTest.java index 1669c1660..e818c5ee3 100644 --- a/src/androidTest/java/tests/storage/SplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/SplitsStorageTest.java @@ -1,5 +1,8 @@ package tests.storage; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; @@ -282,8 +285,8 @@ public void updatedSplitTrafficType() { mSplitsStorage.update(new ProcessedSplitChange(Arrays.asList(s2), empty, 1L, 0L)); mSplitsStorage.update(new ProcessedSplitChange(empty, Arrays.asList(s2ar), 1L, 0L)); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("tt")); - Assert.assertFalse(mSplitsStorage.isValidTrafficType("mytt")); + assertTrue(mSplitsStorage.isValidTrafficType("tt")); + assertFalse(mSplitsStorage.isValidTrafficType("mytt")); } @Test @@ -300,8 +303,8 @@ public void changedTrafficTypeForSplit() { mSplitsStorage.update(new ProcessedSplitChange(Arrays.asList(s1t1), empty, 1L, 0L)); mSplitsStorage.update(new ProcessedSplitChange(Arrays.asList(s1t2), empty, 1L, 0L)); - Assert.assertFalse(mSplitsStorage.isValidTrafficType("tt")); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("mytt")); + assertFalse(mSplitsStorage.isValidTrafficType("tt")); + assertTrue(mSplitsStorage.isValidTrafficType("mytt")); } @Test @@ -320,8 +323,8 @@ public void existingChangedTrafficTypeForSplit() { mSplitsStorage.update(new ProcessedSplitChange(Arrays.asList(s1t1), empty, 1L, 0L)); mSplitsStorage.update(new ProcessedSplitChange(Arrays.asList(s1t2), empty, 1L, 0L)); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("tt")); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("mytt")); + assertTrue(mSplitsStorage.isValidTrafficType("tt")); + assertTrue(mSplitsStorage.isValidTrafficType("mytt")); } @Test @@ -331,9 +334,9 @@ public void trafficTypesAreLoadedInMemoryWhenLoadingLocalSplits() { mSplitsStorage.loadLocal(); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("test_type")); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("test_type_2")); - Assert.assertFalse(mSplitsStorage.isValidTrafficType("invalid_type")); + assertTrue(mSplitsStorage.isValidTrafficType("test_type")); + assertTrue(mSplitsStorage.isValidTrafficType("test_type_2")); + assertFalse(mSplitsStorage.isValidTrafficType("invalid_type")); } @Test @@ -346,9 +349,9 @@ public void loadedFromStorageTrafficTypesAreCorrectlyUpdated() { Split updatedSplit = newSplit("split_test", Status.ACTIVE, "new_type"); mSplitsStorage.update(new ProcessedSplitChange(Collections.singletonList(updatedSplit), Collections.emptyList(), 1L, 0L)); - Assert.assertFalse(mSplitsStorage.isValidTrafficType("test_type")); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("new_type")); - Assert.assertTrue(mSplitsStorage.isValidTrafficType("test_type_2")); + assertFalse(mSplitsStorage.isValidTrafficType("test_type")); + assertTrue(mSplitsStorage.isValidTrafficType("new_type")); + assertTrue(mSplitsStorage.isValidTrafficType("test_type_2")); } @Test @@ -382,7 +385,7 @@ public void flagSetsAreRemovedWhenUpdating() { Collections.singletonList(newSplit("split_test", Status.ACTIVE, "test_type")), Collections.emptyList(), 1L, 0L)); - Assert.assertFalse(initialSet1.isEmpty()); + assertFalse(initialSet1.isEmpty()); Assert.assertEquals(Collections.emptySet(), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); Assert.assertEquals(initialSet2, mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2"))); } @@ -398,11 +401,63 @@ public void updateWithoutChecksRemovesFromFlagSet() { mSplitsStorage.updateWithoutChecks(newSplit("split_test", Status.ACTIVE, "test_type")); - Assert.assertFalse(initialSet1.isEmpty()); + assertFalse(initialSet1.isEmpty()); Assert.assertEquals(Collections.emptySet(), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); Assert.assertEquals(initialSet2, mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_2"))); } + @Test + public void updateReturnsTrueWhenFlagsHaveBeenRemovedFromStorage() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + ArrayList archivedSplits = new ArrayList<>(); + archivedSplits.add(newSplit("split_test", Status.ARCHIVED, "test_type")); + boolean update = mSplitsStorage.update(new ProcessedSplitChange(new ArrayList<>(), archivedSplits, 1L, 0L)); + + assertTrue(update); + } + + @Test + public void updateReturnsTrueWhenFlagsWereAddedToStorage() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + ArrayList activeSplits = new ArrayList<>(); + activeSplits.add(newSplit("split_test_3", Status.ACTIVE, "test_type_2", Collections.singleton("set_2"))); + boolean update = mSplitsStorage.update(new ProcessedSplitChange(activeSplits, new ArrayList<>(), 1L, 0L)); + + assertTrue(update); + } + + @Test + public void updateReturnsTrueWhenFlagsWereUpdatedInStorage() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + ArrayList activeSplits = new ArrayList<>(); + activeSplits.add(newSplit("split_test", Status.ACTIVE, "test_type", Collections.singleton("set_2"))); + boolean update = mSplitsStorage.update(new ProcessedSplitChange(activeSplits, new ArrayList<>(), 1L, 0L)); + + assertTrue(update); + } + + @Test + public void updateReturnsFalseWhenFlagsThatAreNotInStorageAreAttemptedToBeRemoved() { + mRoomDb.clearAllTables(); + mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + mSplitsStorage.loadLocal(); + + ArrayList archivedSplits = new ArrayList<>(); + archivedSplits.add(newSplit("split_test_3", Status.ACTIVE, "test_type_2", Collections.singleton("set_2"))); + boolean update = mSplitsStorage.update(new ProcessedSplitChange(new ArrayList<>(), archivedSplits, 1L, 0L)); + + assertFalse(update); + } + private Split newSplit(String name, Status status, String trafficType) { return newSplit(name, status, trafficType, Collections.emptySet()); } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java index 989d9a16c..de3f5e085 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java @@ -89,7 +89,8 @@ public Map getAll() { } @Override - public void update(ProcessedSplitChange splitChange) { + public boolean update(ProcessedSplitChange splitChange) { + return false; } @Override diff --git a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java index c34dd6539..bf61a1ca5 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -44,9 +44,11 @@ public SplitInPlaceUpdateTask(@NonNull SplitsStorage splitsStorage, public SplitTaskExecutionInfo execute() { try { ProcessedSplitChange processedSplitChange = mSplitChangeProcessor.process(mSplit, mChangeNumber); - mSplitsStorage.update(processedSplitChange); + boolean triggerSdkUpdate = mSplitsStorage.update(processedSplitChange); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + if (triggerSdkUpdate) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + } mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); Logger.v("Updated feature flag"); diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java index 62af70e7b..1e6fa533e 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java @@ -19,7 +19,8 @@ public interface SplitsStorage { Map getAll(); - void update(ProcessedSplitChange splitChange); + // Returns true if at least one split was updated + boolean update(ProcessedSplitChange splitChange); void updateWithoutChecks(Split split); diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java index b143c209c..2d8823356 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java @@ -77,14 +77,20 @@ public Map getAll() { @Override @WorkerThread - public void update(ProcessedSplitChange splitChange) { + public boolean update(ProcessedSplitChange splitChange) { if (splitChange == null) { - return; + return false; } + boolean appliedUpdates = false; + List activeSplits = splitChange.getActiveSplits(); List archivedSplits = splitChange.getArchivedSplits(); if (activeSplits != null) { + if (!activeSplits.isEmpty()) { + // There is at least one added or modified feature flag + appliedUpdates = true; + } for (Split split : activeSplits) { Split loadedSplit = mInMemorySplits.get(split.name); if (loadedSplit != null && loadedSplit.trafficTypeName != null) { @@ -99,6 +105,8 @@ public void update(ProcessedSplitChange splitChange) { if (archivedSplits != null) { for (Split split : archivedSplits) { if (mInMemorySplits.remove(split.name) != null) { + // The flag was in memory, so it will be updated + appliedUpdates = true; decreaseTrafficTypeCount(split.trafficTypeName); deleteFromFlagSetsIfNecessary(split); } @@ -108,6 +116,8 @@ public void update(ProcessedSplitChange splitChange) { mChangeNumber = splitChange.getChangeNumber(); mUpdateTimestamp = splitChange.getUpdateTimestamp(); mPersistentStorage.update(splitChange); + + return appliedUpdates; } @Override diff --git a/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java index 1ba725bbb..90ecd75b5 100644 --- a/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -61,7 +61,6 @@ public void sseUpdateIsRecordedInTelemetryWhenOperationIsSuccessful() { verify(mSplitChangeProcessor).process(mSplit, 123L); verify(mSplitsStorage).update(processedSplitChange); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); assertEquals(result.getStatus(), SplitTaskExecutionStatus.SUCCESS); @@ -98,4 +97,34 @@ public void exceptionDuringStorageUpdateReturnsErrorExecutionInfo() { assertEquals(result.getStatus(), SplitTaskExecutionStatus.ERROR); } + + @Test + public void sdkUpdateIsNotTriggeredWhenStorageUpdateReturnsFalse() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + when(mSplitsStorage.update(processedSplitChange)).thenReturn(false); + + SplitTaskExecutionInfo result = mSplitInPlaceUpdateTask.execute(); + + verify(mSplitChangeProcessor).process(mSplit, 123L); + verify(mSplitsStorage).update(processedSplitChange); + verify(mEventsManager, never()).notifyInternalEvent(any()); + verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + } + + @Test + public void sdkUpdateIsTriggeredWhenStorageUpdateReturnsTrue() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + when(mSplitsStorage.update(processedSplitChange)).thenReturn(true); + + SplitTaskExecutionInfo result = mSplitInPlaceUpdateTask.execute(); + + verify(mSplitChangeProcessor).process(mSplit, 123L); + verify(mSplitsStorage).update(processedSplitChange); + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + } } From ac356793e118c34df9f6c6334aeb4d9222490152 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 31 Oct 2023 17:55:20 -0300 Subject: [PATCH 4/6] Version 3.4.0-rc2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a5245952a..fad0dfa50 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.4.0-alpha-2' + splitVersion = '3.4.0-rc2' } android { From 34ef3e12dc52cfe26200b98231f07eac142d29ba Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 31 Oct 2023 20:03:09 -0300 Subject: [PATCH 5/6] Version 3.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fad0dfa50..9bfa6ed06 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.4.0-rc2' + splitVersion = '3.4.0' } android { From 3df0008e13503a16cc93c4a06e901170eef7b5bc Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 1 Nov 2023 10:27:25 -0300 Subject: [PATCH 6/6] Update CHANGES --- CHANGES.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 51c5c684e..d6f2a18c8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +3.4.0 (Oct 31, 2023) +- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - getTreatmentsByFlagSet and getTreatmentsByFlagSets + - getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets + - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. +- Updated the following SDK manager method to expose flag sets on flag views: + - Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager. + 3.3.0 (Jul 18, 2023) - Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. - Added logic to do a full check of feature flags immediately when the app comes back to foreground, limited to once per minute.