diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b43f4a5dc..1dce9054c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: branches: - master - development + - '*_baseline' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} diff --git a/client/pom.xml b/client/pom.xml index 88f7fdbd2..6d5bab624 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -167,6 +167,11 @@ + + io.split.client + tracker + ${project.version} + io.split.client targeting-engine diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index e71a78ecb..05f21d856 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -9,6 +9,8 @@ import io.split.client.events.EventsTask; import io.split.client.events.InMemoryEventsStorage; import io.split.client.events.NoopEventsStorageImp; +import io.split.client.events.EventQueueStats; +import io.split.client.events.TelemetryEventQueueStats; import io.split.client.impressions.AsynchronousImpressionListener; import io.split.client.impressions.HttpImpressionsSender; import io.split.client.impressions.ImpressionCounter; @@ -254,7 +256,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn _impressionsManager = buildImpressionsManager(config, impressionsStorage, impressionsStorage); // EventClient - EventsStorage eventsStorage = new InMemoryEventsStorage(config.eventsQueueSize(), _telemetryStorageProducer); + EventQueueStats eventsQueueStats = new TelemetryEventQueueStats(_telemetryStorageProducer); + EventsStorage eventsStorage = new InMemoryEventsStorage(config.eventsQueueSize(), eventsQueueStats); EventsSender eventsSender = EventsSender.create(_splitHttpClient, _eventsRootTarget, _telemetryStorageProducer); _eventsTask = EventsTask.create(config.eventSendIntervalInMillis(), eventsStorage, eventsSender, config.getThreadFactory()); diff --git a/client/src/main/java/io/split/client/events/EventsSender.java b/client/src/main/java/io/split/client/events/EventsSender.java index d83969dc8..3b4f4b763 100644 --- a/client/src/main/java/io/split/client/events/EventsSender.java +++ b/client/src/main/java/io/split/client/events/EventsSender.java @@ -14,7 +14,7 @@ import static com.google.common.base.Preconditions.checkNotNull; -public class EventsSender { +public class EventsSender implements io.split.client.events.EventSender { private static final String BULK_ENDPOINT_PATH = "api/events/bulk"; private final URI _bulkEndpoint; @@ -34,10 +34,15 @@ public static EventsSender create(SplitHttpClient splitHttpclient, URI eventsTar _httpPostImp = new HttpPostImp(_client, telemetryRuntimeProducer); } - public void sendEvents(List _data) { + @Override + public void send(List _data) { _httpPostImp.post(_bulkEndpoint, _data, "Events ", HttpParamsWrapper.EVENTS); } + public void sendEvents(List _data) { + send(_data); + } + @VisibleForTesting URI getBulkEndpoint() { return _bulkEndpoint; diff --git a/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java b/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java new file mode 100644 index 000000000..f86a5676c --- /dev/null +++ b/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java @@ -0,0 +1,25 @@ +package io.split.client.events; + +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; + +import java.util.Objects; + +public class TelemetryEventQueueStats implements EventQueueStats { + + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + + public TelemetryEventQueueStats(TelemetryRuntimeProducer telemetryRuntimeProducer) { + _telemetryRuntimeProducer = Objects.requireNonNull(telemetryRuntimeProducer); + } + + @Override + public void onQueued(long count) { + _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, count); + } + + @Override + public void onDropped(long count) { + _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, count); + } +} diff --git a/client/src/test/java/io/split/client/SplitManagerImplTest.java b/client/src/test/java/io/split/client/SplitManagerImplTest.java index a09b7ae1b..1f30d465b 100644 --- a/client/src/test/java/io/split/client/SplitManagerImplTest.java +++ b/client/src/test/java/io/split/client/SplitManagerImplTest.java @@ -88,7 +88,9 @@ public void splitCallWithExistentSplit() { Assert.assertEquals("off", theOne.treatments.get(0)); Assert.assertEquals(0, theOne.configs.size()); Assert.assertEquals("off", theOne.defaultTreatment); - Assert.assertEquals(Lists.newArrayList(prereq), theOne.prerequisites); + Assert.assertEquals(1, theOne.prerequisites.size()); + Assert.assertEquals(prereq.featureFlagName, theOne.prerequisites.get(0).featureFlagName); + Assert.assertEquals(prereq.treatments, theOne.prerequisites.get(0).treatments); } @Test diff --git a/client/src/test/java/io/split/client/events/EventsTaskTest.java b/client/src/test/java/io/split/client/events/EventsTaskTest.java deleted file mode 100644 index 93d5d0d50..000000000 --- a/client/src/test/java/io/split/client/events/EventsTaskTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package io.split.client.events; - -import io.split.client.dtos.Event; -import io.split.telemetry.storage.TelemetryRuntimeProducer; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -public class EventsTaskTest { - private static final EventsSender EVENTS_SENDER = Mockito.mock(EventsSender.class); - - @Test - public void testEventsAreSending() throws InterruptedException { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(10000, telemetryRuntimeProducer); - EventsSender eventsSender = Mockito.mock(EventsSender.class); - EventsTask eventClient = new EventsTask(eventsStorage, - 2000, - eventsSender, - null); - eventClient.start(); - - for (int i = 0; i < 159; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - } - - Thread.sleep(1000); - - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - Thread.sleep(2000); - Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject()); - } - - @Test - public void testEventsWhenCloseTask() throws InterruptedException { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - EventsSender eventsSender = Mockito.mock(EventsSender.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(10000, telemetryRuntimeProducer); - EventsTask eventClient = new EventsTask(eventsStorage, - 2000, - eventsSender, - null); - - for (int i = 0; i < 159; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - } - - eventClient.close(); - Thread.sleep(2000); - Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject()); - } - - @Test - public void testCheckQueFull() { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(10, telemetryRuntimeProducer); - EventsTask eventClient = new EventsTask(eventsStorage, - 2000, - EVENTS_SENDER, - null); - - for (int i = 0; i < 10; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - } - Assert.assertTrue(eventsStorage.isFull()); - } - - @Test - public void testTimesSendingEvents() throws InterruptedException { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - EventsSender eventsSender = Mockito.mock(EventsSender.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(100, telemetryRuntimeProducer); - EventsTask eventClient = new EventsTask(eventsStorage, - 2000, - eventsSender, - null); - eventClient.start(); - - for (int i = 0; i < 10; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - } - - Thread.sleep(3000); - Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject()); - - for (int i = 0; i < 10; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1024 * 32); - } - - Thread.sleep(3000); - Mockito.verify(eventsSender, Mockito.times(2)).sendEvents(Mockito.anyObject()); - eventClient.close(); - Thread.sleep(1000); - Mockito.verify(eventsSender, Mockito.times(2)).sendEvents(Mockito.anyObject()); - } -} \ No newline at end of file diff --git a/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java b/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java deleted file mode 100644 index 3fe8e41c1..000000000 --- a/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.split.client.events; - -import io.split.client.dtos.Event; -import io.split.telemetry.domain.enums.EventsDataRecordsEnum; -import io.split.telemetry.storage.TelemetryRuntimeProducer; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.concurrent.BlockingQueue; - -public class InMemoryEventsStorageTest{ - - @Test - public void testDropEvent() { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer); - - for (int i = 0; i < 3; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1); - } - - Mockito.verify(telemetryRuntimeProducer, Mockito.times(2)).recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1); - Mockito.verify(telemetryRuntimeProducer, Mockito.times(1)).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); - } - - @Test - public void testTrackAndPop() { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - InMemoryEventsStorage eventsStorage = new InMemoryEventsStorage(10, telemetryRuntimeProducer); - - for (int i = 0; i < 5; ++i) { - Event event = new Event(); - eventsStorage.track(event, 1); - } - - Assert.assertEquals(5, eventsStorage.queueSize()); - Assert.assertNotNull(eventsStorage.pop()); - } - - @Test - public void testPopFailed() throws NoSuchFieldException, IllegalAccessException, InterruptedException { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer); - Field eventsQueue = InMemoryEventsStorage.class.getDeclaredField("_eventQueue"); - eventsQueue.setAccessible(true); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(eventsQueue, eventsQueue.getModifiers() & ~Modifier.FINAL); - eventsQueue.set(eventsStorage, blockingQueue); - Mockito.when(blockingQueue.take()).thenThrow(new InterruptedException()); - Assert.assertNull(eventsStorage.pop()); - } - - @Test - public void testTrackException() throws NoSuchFieldException, IllegalAccessException { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer); - - Field eventsQueue = InMemoryEventsStorage.class.getDeclaredField("_eventQueue"); - eventsQueue.setAccessible(true); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(eventsQueue, eventsQueue.getModifiers() & ~Modifier.FINAL); - eventsQueue.set(eventsStorage, blockingQueue); - Mockito.when(blockingQueue.offer(Mockito.anyObject())).thenThrow(new ClassCastException()); - - - Assert.assertEquals(false, eventsStorage.track(new Event(), 1)); - Mockito.verify(telemetryRuntimeProducer, Mockito.times(1)).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); - } - - @Test - public void testEventNullThenFalse() { - TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); - BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class); - EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer); - Assert.assertEquals(false, eventsStorage.track(null, 1)); - } -} \ No newline at end of file diff --git a/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java b/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java new file mode 100644 index 000000000..fcc9aff90 --- /dev/null +++ b/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java @@ -0,0 +1,32 @@ +package io.split.client.events; + +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.Mockito.verify; + +public class TelemetryEventQueueStatsTest { + + @Test + public void onQueuedRecordsEventStats() { + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); + TelemetryEventQueueStats stats = new TelemetryEventQueueStats(telemetryRuntimeProducer); + + stats.onQueued(42); + + verify(telemetryRuntimeProducer).recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 42); + } + + @Test + public void onDroppedRecordsEventStats() { + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); + TelemetryEventQueueStats stats = new TelemetryEventQueueStats(telemetryRuntimeProducer); + + stats.onDropped(10); + + verify(telemetryRuntimeProducer).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 10); + } + +} diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java index fd0faf25a..7e2e7360d 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -50,7 +51,7 @@ public void before() { _segmentCacheConsumer = Mockito.mock(SegmentCacheConsumer.class); _ruleBasedSegmentCacheConsumer = Mockito.mock(RuleBasedSegmentCacheConsumer.class); _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, new FallbackTreatmentCalculatorImp(null)); - _matcher = Mockito.mock(CombiningMatcher.class); + _matcher = CombiningMatcher.of(new io.split.rules.matchers.AllKeysMatcher()); _evaluationContext = Mockito.mock(EvaluationContext.class); _configurations = new HashMap<>(); @@ -104,7 +105,6 @@ public void evaluateWithRollOutConditionBucketIsBiggerTrafficAllocationReturnDef ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 10, 12, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - Mockito.when(condition.matcher().match(MATCHING_KEY, BUCKETING_KEY, null, _evaluationContext)).thenReturn(true); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); @@ -125,7 +125,6 @@ public void evaluateWithRollOutConditionTrafficAllocationIsBiggerBucketReturnTre ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); @@ -146,7 +145,6 @@ public void evaluateWithWhitelistConditionReturnTreatment() { ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); @@ -200,23 +198,16 @@ public void evaluateWithPrerequisites() { List prerequisites = Arrays.asList(new Prerequisite("split1", Arrays.asList(TREATMENT_VALUE))); ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites)); - ParsedSplit split1 = new ParsedSplit("split1", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); + ParsedSplit split1 = new ParsedSplit("split1", 0, false, TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); Mockito.when(_splitCacheConsumer.get("split1")).thenReturn(split1); - Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); assertEquals(TREATMENT_VALUE, result.treatment); assertEquals("test whitelist label", result.label); assertEquals(CHANGE_NUMBER, result.changeNumber); - Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(false); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); - assertEquals(DEFAULT_TREATMENT_VALUE, result.treatment); - assertEquals(Labels.PREREQUISITES_NOT_MET, result.label); - assertEquals(CHANGE_NUMBER, result.changeNumber); - // if split is killed, label should be killed. split = new ParsedSplit(SPLIT_NAME, 0, true, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); @@ -237,15 +228,8 @@ public void evaluateFallbackTreatmentWorks() { assertEquals("on", result.treatment); assertEquals("fallback - definition not found", result.label); - ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null); - Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); - assertEquals("on", result.treatment); - assertEquals("fallback - exception", result.label); - - // using byflag only + // using by-flag fallback Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(null); - Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(null); fallbackTreatmentsConfiguration = new FallbackTreatmentsConfiguration(new HashMap() {{ put(SPLIT_NAME, new FallbackTreatment("off")); }} ); fallbackTreatmentCalculator = new FallbackTreatmentCalculatorImp(fallbackTreatmentsConfiguration); _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, fallbackTreatmentCalculator); @@ -253,48 +237,5 @@ public void evaluateFallbackTreatmentWorks() { result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); assertEquals("off", result.treatment); assertEquals("fallback - definition not found", result.label); - - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null); - assertEquals("control", result.treatment); - assertEquals("definition not found", result.label); - - split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null); - Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); - assertEquals("off", result.treatment); - assertEquals("fallback - exception", result.label); - - split = new ParsedSplit("another_name", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null); - Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(split); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null); - assertEquals("control", result.treatment); - assertEquals("exception", result.label); - - // with byflag - Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(null); - Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(null); - fallbackTreatmentsConfiguration = new FallbackTreatmentsConfiguration(new FallbackTreatment("on"), new HashMap() {{ put(SPLIT_NAME, new FallbackTreatment("off")); }} ); - fallbackTreatmentCalculator = new FallbackTreatmentCalculatorImp(fallbackTreatmentsConfiguration); - _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, fallbackTreatmentCalculator); - - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); - assertEquals("off", result.treatment); - assertEquals("fallback - definition not found", result.label); - - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null); - assertEquals("on", result.treatment); - assertEquals("fallback - definition not found", result.label); - - split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null); - Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); - assertEquals("off", result.treatment); - assertEquals("fallback - exception", result.label); - - split = new ParsedSplit("another_name", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null); - Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(split); - result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null); - assertEquals("on", result.treatment); - assertEquals("fallback - exception", result.label); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java index df09569d0..efcb386ce 100644 --- a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java @@ -431,7 +431,7 @@ public void EqualToSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" == semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" == semver 1.22.9")); return; } } @@ -453,7 +453,7 @@ public void GreaterThanOrEqualSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("greater than or equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" >= semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" >= semver 1.22.9")); return; } } @@ -475,7 +475,7 @@ public void LessThanOrEqualSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("less than or equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" <= semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" <= semver 1.22.9")); return; } } @@ -497,7 +497,7 @@ public void BetweenSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("between semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" between semver 1\\.22\\.9 and 2\\.1\\.0")); + assertTrue(matcher.matcher().toString().equals(" between semver 1.22.9 and 2.1.0")); return; } } 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 068d60ccc..b7649cbfe 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java @@ -561,7 +561,7 @@ public void EqualToSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" == semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" == semver 1.22.9")); return; } } @@ -583,7 +583,7 @@ public void GreaterThanOrEqualSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("greater than or equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" >= semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" >= semver 1.22.9")); return; } } @@ -605,7 +605,7 @@ public void LessThanOrEqualSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("less than or equal to semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" <= semver 1\\.22\\.9")); + assertTrue(matcher.matcher().toString().equals(" <= semver 1.22.9")); return; } } @@ -627,7 +627,7 @@ public void BetweenSemverMatcher() throws IOException { assertTrue(parsedCondition.label().equals("between semver")); for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { // Check the matcher is ALL_KEYS - assertTrue(matcher.matcher().toString().equals(" between semver 1\\.22\\.9 and 2\\.1\\.0")); + assertTrue(matcher.matcher().toString().equals(" between semver 1.22.9 and 2.1.0")); return; } } diff --git a/client/src/test/java/io/split/engine/matchers/SemverTest.java b/client/src/test/java/io/split/engine/matchers/SemverTest.java index 147533b7d..020e2a2be 100644 --- a/client/src/test/java/io/split/engine/matchers/SemverTest.java +++ b/client/src/test/java/io/split/engine/matchers/SemverTest.java @@ -108,7 +108,7 @@ public void testCompareVersions() throws IOException { } @Test public void testLeadingZeros() { - assertTrue(Semver.build("1.01.2").version().equals("1\\.1\\.2")); - assertTrue(Semver.build("1.01.2-rc.01").version().equals("1\\.1\\.2-rc\\.1")); + assertTrue(Semver.build("1.01.2").version().equals("1.1.2")); + assertTrue(Semver.build("1.01.2-rc.01").version().equals("1.1.2-rc.1")); } } diff --git a/parsing-commons/pom.xml b/parsing-commons/pom.xml index f16bbb366..fe3871272 100644 --- a/parsing-commons/pom.xml +++ b/parsing-commons/pom.xml @@ -29,7 +29,19 @@ org.mockito mockito-core - 5.14.2 + 1.10.19 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + org.powermock + powermock-api-mockito + 1.7.4 test diff --git a/pom.xml b/pom.xml index e9e75f72f..c17dd8e6d 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,7 @@ redis-wrapper testing okhttp-modules + tracker client @@ -87,6 +88,7 @@ org.apache.maven.plugins maven-source-plugin + 3.4.0 attach-sources diff --git a/segment-commons/pom.xml b/segment-commons/pom.xml index eb0c2ccc1..b7d227fba 100644 --- a/segment-commons/pom.xml +++ b/segment-commons/pom.xml @@ -37,7 +37,19 @@ org.mockito mockito-core - 5.14.2 + 1.10.19 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + org.powermock + powermock-api-mockito + 1.7.4 test diff --git a/targeting-engine/pom.xml b/targeting-engine/pom.xml index d362412df..5bf0944a7 100644 --- a/targeting-engine/pom.xml +++ b/targeting-engine/pom.xml @@ -24,7 +24,19 @@ org.mockito mockito-core - 5.14.2 + 1.10.19 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + org.powermock + powermock-api-mockito + 1.7.4 test diff --git a/tracker/pom.xml b/tracker/pom.xml new file mode 100644 index 000000000..e29fd22d7 --- /dev/null +++ b/tracker/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + io.split.client + java-client-parent + 4.18.3 + + + tracker + jar + Tracker + Shared event queue, scheduler, and HTTP-injected delivery + + + + com.google.code.gson + gson + 2.13.1 + + + org.slf4j + slf4j-api + 1.7.36 + + + junit + junit + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.powermock + powermock-module-junit4 + 1.7.4 + test + + + org.powermock + powermock-api-mockito + 1.7.4 + test + + + diff --git a/client/src/main/java/io/split/client/dtos/Event.java b/tracker/src/main/java/io/split/client/dtos/Event.java similarity index 82% rename from client/src/main/java/io/split/client/dtos/Event.java rename to tracker/src/main/java/io/split/client/dtos/Event.java index daf1c66d4..d9d4a7bfc 100644 --- a/client/src/main/java/io/split/client/dtos/Event.java +++ b/tracker/src/main/java/io/split/client/dtos/Event.java @@ -1,9 +1,9 @@ package io.split.client.dtos; -import com.google.common.base.Objects; import com.google.gson.annotations.SerializedName; import java.util.Map; +import java.util.Objects; public class Event { @@ -36,13 +36,13 @@ public boolean equals(Object o) { Event event = (Event) o; return Double.compare(event.value, value) == 0 && timestamp == event.timestamp && - Objects.equal(eventTypeId, event.eventTypeId) && - Objects.equal(trafficTypeName, event.trafficTypeName) && - Objects.equal(key, event.key); + Objects.equals(eventTypeId, event.eventTypeId) && + Objects.equals(trafficTypeName, event.trafficTypeName) && + Objects.equals(key, event.key); } @Override public int hashCode() { - return Objects.hashCode(eventTypeId, trafficTypeName, key, value, timestamp); + return Objects.hash(eventTypeId, trafficTypeName, key, value, timestamp); } } diff --git a/tracker/src/main/java/io/split/client/events/EventQueueStats.java b/tracker/src/main/java/io/split/client/events/EventQueueStats.java new file mode 100644 index 000000000..83e39763c --- /dev/null +++ b/tracker/src/main/java/io/split/client/events/EventQueueStats.java @@ -0,0 +1,6 @@ +package io.split.client.events; + +public interface EventQueueStats { + void onQueued(long count); + void onDropped(long count); +} diff --git a/tracker/src/main/java/io/split/client/events/EventSender.java b/tracker/src/main/java/io/split/client/events/EventSender.java new file mode 100644 index 000000000..1282cdb78 --- /dev/null +++ b/tracker/src/main/java/io/split/client/events/EventSender.java @@ -0,0 +1,8 @@ +package io.split.client.events; + +import io.split.client.dtos.Event; +import java.util.List; + +public interface EventSender { + void send(List events); +} diff --git a/client/src/main/java/io/split/client/events/EventsStorage.java b/tracker/src/main/java/io/split/client/events/EventsStorage.java similarity index 100% rename from client/src/main/java/io/split/client/events/EventsStorage.java rename to tracker/src/main/java/io/split/client/events/EventsStorage.java diff --git a/client/src/main/java/io/split/client/events/EventsStorageConsumer.java b/tracker/src/main/java/io/split/client/events/EventsStorageConsumer.java similarity index 100% rename from client/src/main/java/io/split/client/events/EventsStorageConsumer.java rename to tracker/src/main/java/io/split/client/events/EventsStorageConsumer.java diff --git a/client/src/main/java/io/split/client/events/EventsStorageProducer.java b/tracker/src/main/java/io/split/client/events/EventsStorageProducer.java similarity index 100% rename from client/src/main/java/io/split/client/events/EventsStorageProducer.java rename to tracker/src/main/java/io/split/client/events/EventsStorageProducer.java diff --git a/client/src/main/java/io/split/client/events/EventsTask.java b/tracker/src/main/java/io/split/client/events/EventsTask.java similarity index 67% rename from client/src/main/java/io/split/client/events/EventsTask.java rename to tracker/src/main/java/io/split/client/events/EventsTask.java index fb71a7241..de9833955 100644 --- a/client/src/main/java/io/split/client/events/EventsTask.java +++ b/tracker/src/main/java/io/split/client/events/EventsTask.java @@ -1,54 +1,46 @@ package io.split.client.events; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.split.client.dtos.Event; -import io.split.client.utils.SplitExecutorFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; +import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import static com.google.common.base.Preconditions.checkNotNull; - /** * Responsible for sending events added via .track() to Split collection services */ public class EventsTask{ private final EventsStorageConsumer _eventsStorageConsumer; - private final EventsSender _eventsSender; + private final EventSender _eventsSender; private final long _sendIntervalMillis; private final ScheduledExecutorService _senderScheduledExecutorService; private static final Logger _log = LoggerFactory.getLogger(EventsTask.class); - public static EventsTask create(long sendIntervalMillis, EventsStorageConsumer eventsStorageConsumer, EventsSender eventsSender, - ThreadFactory threadFactory) throws URISyntaxException { + public static EventsTask create(long sendIntervalMillis, EventsStorageConsumer eventsStorageConsumer, EventSender eventsSender, + ThreadFactory threadFactory) { return new EventsTask(eventsStorageConsumer, sendIntervalMillis, eventsSender, threadFactory); } - EventsTask(EventsStorageConsumer eventsStorageConsumer, - long sendIntervalMillis, EventsSender eventsSender, ThreadFactory threadFactory) { + public EventsTask(EventsStorageConsumer eventsStorageConsumer, + long sendIntervalMillis, EventSender eventsSender, ThreadFactory threadFactory) { - _eventsStorageConsumer = checkNotNull(eventsStorageConsumer); + _eventsStorageConsumer = Objects.requireNonNull(eventsStorageConsumer); _sendIntervalMillis = sendIntervalMillis; - _eventsSender = checkNotNull(eventsSender); - _senderScheduledExecutorService = SplitExecutorFactory.buildSingleThreadScheduledExecutor(threadFactory, "Sender-events-%d"); - } - - ThreadFactory eventClientThreadFactory(final String name) { - return new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(name) - .build(); + _eventsSender = Objects.requireNonNull(eventsSender); + _senderScheduledExecutorService = threadFactory != null + ? Executors.newSingleThreadScheduledExecutor(threadFactory) + : Executors.newSingleThreadScheduledExecutor(); } public void start(){ @@ -85,6 +77,6 @@ void sendEvents(){ if (eventsToSend.isEmpty()){ return; } - _eventsSender.sendEvents(eventsToSend); + _eventsSender.send(eventsToSend); } -} \ No newline at end of file +} diff --git a/client/src/main/java/io/split/client/events/InMemoryEventsStorage.java b/tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java similarity index 69% rename from client/src/main/java/io/split/client/events/InMemoryEventsStorage.java rename to tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java index a3463f0ed..fa7523f90 100644 --- a/client/src/main/java/io/split/client/events/InMemoryEventsStorage.java +++ b/tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java @@ -1,30 +1,26 @@ package io.split.client.events; -import com.google.common.annotations.VisibleForTesting; import io.split.client.dtos.Event; -import io.split.telemetry.domain.enums.EventsDataRecordsEnum; -import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import static com.google.common.base.Preconditions.checkNotNull; - public class InMemoryEventsStorage implements EventsStorage{ private static final Logger _log = LoggerFactory.getLogger(InMemoryEventsStorage.class); private final BlockingQueue _eventQueue; private final int _maxQueueSize; - private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + private final EventQueueStats _stats; - public InMemoryEventsStorage(int maxQueueSize, TelemetryRuntimeProducer telemetryRuntimeProducer) { + public InMemoryEventsStorage(int maxQueueSize, EventQueueStats stats) { _eventQueue = new LinkedBlockingQueue<>(maxQueueSize); _maxQueueSize = maxQueueSize; - _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + _stats = Objects.requireNonNull(stats, "stats must not be null"); } @Override @@ -56,23 +52,22 @@ public boolean track(Event event, int eventSize) { return false; } if(_eventQueue.offer(new WrappedEvent(event, eventSize))) { - _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1); + _stats.onQueued(1); } else { _log.warn("Event queue is full, dropping event."); - _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); + _stats.onDropped(1); return false; } } catch (ClassCastException | NullPointerException | IllegalArgumentException e) { - _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); + _stats.onDropped(1); _log.warn("Interruption when adding event withed while adding message %s.", event); return false; } return true; } - @VisibleForTesting int queueSize() { return _maxQueueSize - _eventQueue.remainingCapacity(); } diff --git a/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java b/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java new file mode 100644 index 000000000..197ce53c3 --- /dev/null +++ b/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java @@ -0,0 +1,14 @@ +package io.split.client.events; + +public final class NoopEventQueueStats implements EventQueueStats { + public static final NoopEventQueueStats INSTANCE = new NoopEventQueueStats(); + private NoopEventQueueStats() {} + + @Override public void onQueued(long count) { + // no-op + } + + @Override public void onDropped(long count) { + // no-op + } +} diff --git a/client/src/main/java/io/split/client/events/NoopEventsStorageImp.java b/tracker/src/main/java/io/split/client/events/NoopEventsStorageImp.java similarity index 100% rename from client/src/main/java/io/split/client/events/NoopEventsStorageImp.java rename to tracker/src/main/java/io/split/client/events/NoopEventsStorageImp.java diff --git a/client/src/main/java/io/split/client/events/WrappedEvent.java b/tracker/src/main/java/io/split/client/events/WrappedEvent.java similarity index 100% rename from client/src/main/java/io/split/client/events/WrappedEvent.java rename to tracker/src/main/java/io/split/client/events/WrappedEvent.java diff --git a/tracker/src/test/java/io/split/client/dtos/EventTest.java b/tracker/src/test/java/io/split/client/dtos/EventTest.java new file mode 100644 index 000000000..8db57d6d2 --- /dev/null +++ b/tracker/src/test/java/io/split/client/dtos/EventTest.java @@ -0,0 +1,234 @@ +package io.split.client.dtos; + +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.Assert.*; + +public class EventTest { + + @Test + public void equalsReturnsTrueForSameObject() { + Event event = new Event(); + assertTrue(event.equals(event)); + } + + @Test + public void equalsReturnsFalseForNull() { + Event event = new Event(); + assertFalse(event.equals(null)); + } + + @Test + public void equalsReturnsFalseForDifferentClass() { + Event event = new Event(); + assertFalse(event.equals("not an event")); + } + + @Test + public void equalsReturnsTrueForIdenticalEvents() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertTrue(event1.equals(event2)); + } + + @Test + public void equalsReturnsFalseForDifferentEventTypeId() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "view"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertFalse(event1.equals(event2)); + } + + @Test + public void equalsReturnsFalseForDifferentTrafficTypeName() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "account"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertFalse(event1.equals(event2)); + } + + @Test + public void equalsReturnsFalseForDifferentKey() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user456"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertFalse(event1.equals(event2)); + } + + @Test + public void equalsReturnsFalseForDifferentValue() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 200.0; + event2.timestamp = 1000L; + + assertFalse(event1.equals(event2)); + } + + @Test + public void equalsReturnsFalseForDifferentTimestamp() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 2000L; + + assertFalse(event1.equals(event2)); + } + + @Test + public void equalsIgnoresPropertiesField() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + event1.properties = new HashMap<>(); + event1.properties.put("color", "red"); + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + event2.properties = new HashMap<>(); + event2.properties.put("size", "large"); + + assertTrue(event1.equals(event2)); + } + + @Test + public void equalsIsSelfConsistent() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertTrue(event1.equals(event2)); + assertTrue(event1.equals(event2)); + } + + @Test + public void equalsIsSymmetric() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + assertTrue(event1.equals(event2)); + assertTrue(event2.equals(event1)); + } + + @Test + public void equalsIsTransitive() { + Event event1 = new Event(); + event1.eventTypeId = "purchase"; + event1.trafficTypeName = "user"; + event1.key = "user123"; + event1.value = 100.0; + event1.timestamp = 1000L; + + Event event2 = new Event(); + event2.eventTypeId = "purchase"; + event2.trafficTypeName = "user"; + event2.key = "user123"; + event2.value = 100.0; + event2.timestamp = 1000L; + + Event event3 = new Event(); + event3.eventTypeId = "purchase"; + event3.trafficTypeName = "user"; + event3.key = "user123"; + event3.value = 100.0; + event3.timestamp = 1000L; + + assertTrue(event1.equals(event2)); + assertTrue(event2.equals(event3)); + assertTrue(event1.equals(event3)); + } + +} diff --git a/tracker/src/test/java/io/split/client/events/EventsTaskTest.java b/tracker/src/test/java/io/split/client/events/EventsTaskTest.java new file mode 100644 index 000000000..875009d44 --- /dev/null +++ b/tracker/src/test/java/io/split/client/events/EventsTaskTest.java @@ -0,0 +1,86 @@ +package io.split.client.events; + +import io.split.client.dtos.Event; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadFactory; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class EventsTaskTest { + + private EventsStorageConsumer _storage; + private EventSender _sender; + private EventsTask _task; + + @Before + public void setup() { + _storage = mock(EventsStorageConsumer.class); + _sender = mock(EventSender.class); + ThreadFactory tf = r -> { + Thread t = new Thread(r, "test-events-thread"); + t.setDaemon(true); + return t; + }; + _task = new EventsTask(_storage, 30_000L, _sender, tf); + } + + @Test + public void sendEventsDoesNothingWhenQueueEmpty() { + when(_storage.popAll()).thenReturn(Collections.emptyList()); + _task.sendEvents(); + verifyZeroInteractions(_sender); + } + + @Test + public void sendEventsBatchesAllQueuedEvents() { + Event e1 = makeEvent("click"); + Event e2 = makeEvent("purchase"); + when(_storage.popAll()).thenReturn(Arrays.asList( + new WrappedEvent(e1, 10), new WrappedEvent(e2, 20))); + + _task.sendEvents(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); + verify(_sender).send(captor.capture()); + assertEquals(2, captor.getValue().size()); + assertTrue(captor.getValue().contains(e1)); + assertTrue(captor.getValue().contains(e2)); + } + + @Test + public void closeFlushesRemainingEventsBeforeShutdown() { + Event e = makeEvent("close-event"); + when(_storage.popAll()).thenReturn( + Collections.singletonList(new WrappedEvent(e, 10))); + + _task.close(); + + verify(_sender, atLeastOnce()).send(anyList()); + } + + @Test + public void sendEventsDoesNotThrowWhenQueueIsFull() { + when(_storage.isFull()).thenReturn(true); + when(_storage.popAll()).thenReturn(Collections.emptyList()); + _task.sendEvents(); + verify(_storage).isFull(); + verify(_storage).popAll(); + verifyZeroInteractions(_sender); + } + + private static Event makeEvent(String type) { + Event e = new Event(); + e.eventTypeId = type; + e.key = "k"; + e.trafficTypeName = "user"; + e.timestamp = System.currentTimeMillis(); + return e; + } +} diff --git a/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java b/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java new file mode 100644 index 000000000..8f2eca6c9 --- /dev/null +++ b/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java @@ -0,0 +1,74 @@ +package io.split.client.events; + +import io.split.client.dtos.Event; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class InMemoryEventsStorageTest { + + private EventQueueStats _stats; + private InMemoryEventsStorage _storage; + + @Before + public void setup() { + _stats = Mockito.mock(EventQueueStats.class); + _storage = new InMemoryEventsStorage(5, _stats); + } + + @Test + public void trackReturnsTrue_andNotifiesQueued() { + assertTrue(_storage.track(makeEvent("myType"), 100)); + verify(_stats).onQueued(1); + verifyNoMoreInteractions(_stats); + } + + @Test + public void trackNullEventReturnsFalse() { + assertFalse(_storage.track(null, 0)); + verifyZeroInteractions(_stats); + } + + @Test + public void isFullWhenCapacityReached() { + for (int i = 0; i < 5; i++) _storage.track(makeEvent("t" + i), 10); + assertTrue(_storage.isFull()); + } + + @Test + public void dropWhenFullNotifiesDropped() { + for (int i = 0; i < 5; i++) _storage.track(makeEvent("t" + i), 10); + assertFalse(_storage.track(makeEvent("overflow"), 10)); + verify(_stats).onDropped(1); + } + + @Test + public void popAllDrainsQueue() { + _storage.track(makeEvent("a"), 10); + _storage.track(makeEvent("b"), 20); + List popped = _storage.popAll(); + assertEquals(2, popped.size()); + assertEquals("a", popped.get(0).event().eventTypeId); + assertEquals("b", popped.get(1).event().eventTypeId); + assertTrue(_storage.popAll().isEmpty()); + } + + @Test + public void popAllReturnsEmptyWhenQueueIsEmpty() { + assertTrue(_storage.popAll().isEmpty()); + } + + private static Event makeEvent(String eventTypeId) { + Event e = new Event(); + e.eventTypeId = eventTypeId; + e.key = "key"; + e.trafficTypeName = "user"; + e.timestamp = System.currentTimeMillis(); + return e; + } +}