diff --git a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java index 6d0f42f4..27cbf040 100644 --- a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java +++ b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java @@ -28,11 +28,7 @@ import pluggable.CustomStorageWrapper; import java.net.URISyntaxException; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -850,4 +846,181 @@ public void testCounterConsumerModeNoneMode() { manager.start(); Assert.assertNotNull(manager.getCounter()); } + + @Test + public void testImpressionToggleStandaloneOptimizedMode() { + SplitClientConfig config = SplitClientConfig.builder() + .impressionsQueueSize(10) + .endpoint("nowhere.com", "nowhere.com") + .impressionsMode(ImpressionsManager.Mode.OPTIMIZED) + .build(); + ImpressionsStorage storage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); + + ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); + ImpressionCounter impressionCounter = new ImpressionCounter(); + ImpressionObserver impressionObserver = new ImpressionObserver(200); + TelemetryStorageProducer telemetryStorageProducer = new InMemoryTelemetryStorage(); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetryInMemorySubmitter.class); + UniqueKeysTracker uniqueKeysTracker = new UniqueKeysTrackerImp(telemetrySynchronizer, 1000, 1000, null); + uniqueKeysTracker.start(); + + ProcessImpressionStrategy processImpressionStrategy = new ProcessImpressionOptimized(false, impressionObserver, impressionCounter, telemetryStorageProducer); + ProcessImpressionNone processImpressionNone = new ProcessImpressionNone(false, uniqueKeysTracker, impressionCounter); + + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(config, senderMock, TELEMETRY_STORAGE, storage, storage, processImpressionNone, processImpressionStrategy, impressionCounter, null); + treatmentLog.start(); + + // These 4 unique test name will cause 4 entries but we are caping at the first 3. + KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); + KeyImpression ki2 = keyImpression("test1", "mati", "on", 2L, 1L); + KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); + KeyImpression ki4 = keyImpression("test1", "bilal", "on", 4L, 1L); + + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.sendImpressions(); + + verify(senderMock).postImpressionsBulk(impressionsCaptor.capture()); + + List captured = impressionsCaptor.getValue(); + Assert.assertEquals(2, captured.get(0).keyImpressions.size()); + for (TestImpressions testImpressions : captured) { + for (KeyImpression keyImpression : testImpressions.keyImpressions) { + Assert.assertEquals(null, keyImpression.previousTime); + } + } + // Only the first 2 impressions make it to the server + Assert.assertTrue(captured.get(0).keyImpressions.contains(keyImpression("test1", "adil", "on", 1L, 1L))); + Assert.assertTrue(captured.get(0).keyImpressions.contains(keyImpression("test1", "pato", "on", 3L, 1L))); + + HashMap> trackedKeys = ((UniqueKeysTrackerImp) uniqueKeysTracker).popAll(); + HashSet keys = new HashSet<>(); + keys.add("mati"); + keys.add("bilal"); + Assert.assertEquals(1, trackedKeys.size()); + Assert.assertEquals(keys, trackedKeys.get("test1")); + + treatmentLog.sendImpressionCounters(); + verify(senderMock).postCounters(impressionCountCaptor.capture()); + HashMap capturedCounts = impressionCountCaptor.getValue(); + Assert.assertEquals(1, capturedCounts.size()); + Assert.assertTrue(capturedCounts.entrySet().contains(new AbstractMap.SimpleEntry<>(new ImpressionCounter.Key("test1", 0), 2))); + + // Assert that the sender is never called if the counters are empty. + Mockito.reset(senderMock); + treatmentLog.sendImpressionCounters(); + verify(senderMock, times(0)).postCounters(Mockito.any()); + } + + @Test + public void testImpressionToggleStandaloneModeDebugMode() { + SplitClientConfig config = SplitClientConfig.builder() + .impressionsQueueSize(10) + .endpoint("nowhere.com", "nowhere.com") + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .build(); + ImpressionsStorage storage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); + + ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); + ImpressionCounter impressionCounter = Mockito.mock(ImpressionCounter.class); + ImpressionObserver impressionObserver = new ImpressionObserver(200); + ProcessImpressionStrategy processImpressionStrategy = new ProcessImpressionDebug(false, impressionObserver); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetryInMemorySubmitter.class); + UniqueKeysTracker uniqueKeysTracker = new UniqueKeysTrackerImp(telemetrySynchronizer, 1000, 1000, null); + uniqueKeysTracker.start(); + ProcessImpressionNone processImpressionNone = new ProcessImpressionNone(false, uniqueKeysTracker, impressionCounter); + + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(config, senderMock, TELEMETRY_STORAGE, storage, storage, processImpressionNone, processImpressionStrategy, impressionCounter, null); + treatmentLog.start(); + + // These 4 unique test name will cause 4 entries but we are caping at the first 3. + KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); + KeyImpression ki2 = keyImpression("test1", "mati", "on", 2L, 1L); + KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); + KeyImpression ki4 = keyImpression("test1", "bilal", "on", 4L, 1L); + + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.sendImpressions(); + + HashMap> trackedKeys = ((UniqueKeysTrackerImp) uniqueKeysTracker).popAll(); + HashSet keys = new HashSet<>(); + keys.add("mati"); + keys.add("bilal"); + Assert.assertEquals(1, trackedKeys.size()); + Assert.assertEquals(keys, trackedKeys.get("test1")); + + verify(senderMock).postImpressionsBulk(impressionsCaptor.capture()); + + List captured = impressionsCaptor.getValue(); + Assert.assertEquals(2, captured.get(0).keyImpressions.size()); + for (TestImpressions testImpressions : captured) { + KeyImpression keyImpression1 = testImpressions.keyImpressions.get(0); + KeyImpression keyImpression3 = testImpressions.keyImpressions.get(1); + Assert.assertEquals(null, keyImpression1.previousTime); + Assert.assertEquals(null, keyImpression3.previousTime); + } + // Only the first 2 impressions make it to the server + Assert.assertTrue(captured.get(0).keyImpressions.contains(keyImpression("test1", "adil", "on", 1L, 1L))); + Assert.assertTrue(captured.get(0).keyImpressions.contains(keyImpression("test1", "pato", "on", 3L, 1L))); + } + + @Test + public void testImpressionToggleStandaloneModeNoneMode() { + SplitClientConfig config = SplitClientConfig.builder() + .impressionsQueueSize(10) + .endpoint("nowhere.com", "nowhere.com") + .impressionsMode(ImpressionsManager.Mode.NONE) + .build(); + ImpressionsStorage storage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); + + ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetryInMemorySubmitter.class); + ImpressionCounter impressionCounter = new ImpressionCounter(); + UniqueKeysTracker uniqueKeysTracker = new UniqueKeysTrackerImp(telemetrySynchronizer, 1000, 1000, null); + uniqueKeysTracker.start(); + + ProcessImpressionStrategy processImpressionStrategy = new ProcessImpressionNone(false, uniqueKeysTracker, impressionCounter); + ProcessImpressionNone processImpressionNone = (ProcessImpressionNone) processImpressionStrategy; + + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(config, senderMock, TELEMETRY_STORAGE, storage, storage, processImpressionNone, processImpressionStrategy, impressionCounter, null); + treatmentLog.start(); + + // These 4 unique test name will cause 4 entries but we are caping at the first 3. + KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); + KeyImpression ki2 = keyImpression("test1", "mati", "on", 2L, 1L); + KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); + KeyImpression ki4 = keyImpression("test1", "bilal", "on", 4L, 1L); + + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null), true)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new DecoratedImpression(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null), false)).collect(Collectors.toList())); + treatmentLog.close(); + HashMap> trackedKeys = ((UniqueKeysTrackerImp) uniqueKeysTracker).popAll(); + uniqueKeysTracker.stop(); + + HashSet keys = new HashSet<>(); + keys.add("adil"); + keys.add("mati"); + keys.add("pato"); + keys.add("bilal"); + Assert.assertEquals(1, trackedKeys.size()); + Assert.assertEquals(keys, trackedKeys.get("test1")); + + //treatmentLog.sendImpressionCounters(); + verify(senderMock).postCounters(impressionCountCaptor.capture()); + HashMap capturedCounts = impressionCountCaptor.getValue(); + Assert.assertEquals(1, capturedCounts.size()); + Assert.assertTrue(capturedCounts.entrySet().contains(new AbstractMap.SimpleEntry<>(new ImpressionCounter.Key("test1", 0), 4))); + + // Assert that the sender is never called if the counters are empty. + Mockito.reset(senderMock); + treatmentLog.sendImpressionCounters(); + verify(senderMock, times(0)).postCounters(Mockito.any()); + } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java index 6c5c699a..2958106d 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java @@ -48,6 +48,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -639,6 +640,26 @@ public void InListSemverMatcher() throws IOException { assertTrue(false); } + @Test + public void ImpressionToggleParseTest() throws IOException { + SplitParser parser = new SplitParser(); + String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(splits, SplitChange.class); + for (Split split : change.splits) { + // should not cause exception + ParsedSplit parsedSplit = parser.parse(split); + if (split.name.equals("without_impression_toggle")) { + assertTrue(split.trackImpression); + } + if (split.name.equals("impression_toggle_on")) { + assertTrue(split.trackImpression); + } + if (split.name.equals("impression_toggle_off")) { + assertFalse(split.trackImpression); + } + } + } + public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); diff --git a/client/src/test/resources/splits_imp_toggle.json b/client/src/test/resources/splits_imp_toggle.json new file mode 100644 index 00000000..4bd499ac --- /dev/null +++ b/client/src/test/resources/splits_imp_toggle.json @@ -0,0 +1,155 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "without_impression_toggle", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "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 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "impression_toggle_on", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "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 + } + ], + "label": "default rule" + } + ], + "trackImpression": true + }, + { + "trafficTypeName": "user", + "name": "impression_toggle_off", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "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 + } + ], + "label": "default rule" + } + ], + "trackImpression": false + } + ], + "since": -1, + "till": 1585948850109 +}