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] 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() ); }