diff --git a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java index c8bd282b7..3d9171dfe 100644 --- a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java +++ b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java @@ -26,7 +26,7 @@ public class EventsManagerCoordinator implements ISplitEventsManager, EventsMana */ private static final Set SDK_SCOPED_EVENTS = EnumSet.of( SplitInternalEvent.SPLITS_UPDATED, - SplitInternalEvent.SPLITS_FETCHED, + SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java index b5a9683b7..4d012e7f8 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -80,20 +80,7 @@ public SplitEventExecutorResources getExecutorResources() { @Override public void notifyInternalEvent(SplitInternalEvent internalEvent) { requireNonNull(internalEvent); - - // Skip FETCHED events after SDK_READY to prevent unnecessary SDK_UPDATE triggers. - // TODO: This is temporary until *_FETCHED and *_UPDATED events are unified. - if ((internalEvent == SplitInternalEvent.SPLITS_FETCHED - || internalEvent == SplitInternalEvent.MY_SEGMENTS_FETCHED) - && eventAlreadyTriggered(SplitEvent.SDK_READY)) { - return; - } - - // Notify the actual internal event mEventsManager.notifyInternalEvent(internalEvent, null); - - // Also notify the synthetic composite events for SDK_READY evaluation - notifySyntheticEventsIfNeeded(internalEvent); } /** @@ -104,16 +91,7 @@ && eventAlreadyTriggered(SplitEvent.SDK_READY)) { */ public void notifyInternalEvent(SplitInternalEvent internalEvent, EventMetadata metadata) { requireNonNull(internalEvent); - - // Skip FETCHED events after SDK_READY - if ((internalEvent == SplitInternalEvent.SPLITS_FETCHED - || internalEvent == SplitInternalEvent.MY_SEGMENTS_FETCHED) - && eventAlreadyTriggered(SplitEvent.SDK_READY)) { - return; - } - mEventsManager.notifyInternalEvent(internalEvent, metadata); - notifySyntheticEventsIfNeeded(internalEvent); } @Override @@ -143,32 +121,6 @@ public void destroy() { mEventsManager.destroy(); } - /** - * Notifies the synthetic composite events based on the actual internal event. - * These synthetic events simplify the SDK_READY condition evaluation. - * The prerequisite configuration ensures SDK_READY_FROM_CACHE always fires before SDK_READY. - *

- * TODO: Remove this method once EventsManagerConfig is updated. - */ - private void notifySyntheticEventsIfNeeded(SplitInternalEvent internalEvent) { - switch (internalEvent) { - case SPLITS_UPDATED: - case SPLITS_FETCHED: - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_SYNC_COMPLETE, null); - break; - - case MY_SEGMENTS_UPDATED: - case MY_SEGMENTS_FETCHED: - case MY_LARGE_SEGMENTS_UPDATED: - mEventsManager.notifyInternalEvent(SplitInternalEvent.SEGMENTS_SYNC_COMPLETE, null); - break; - - default: - // No synthetic event needed for other internal events - break; - } - } - private void startTimeoutThread(final int blockUntilReady) { Thread timeoutThread = new Thread(new Runnable() { @Override diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java index b1999a170..f6c09ac6f 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManagerConfigFactory.java @@ -41,13 +41,13 @@ static EventsManagerConfig create() { cacheGroup.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); Set syncGroup = new HashSet<>(); - syncGroup.add(SplitInternalEvent.SPLITS_SYNC_COMPLETE); - syncGroup.add(SplitInternalEvent.SEGMENTS_SYNC_COMPLETE); + syncGroup.add(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + syncGroup.add(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); return EventsManagerConfig.builder() .requireAll(SplitEvent.SDK_READY, - SplitInternalEvent.SPLITS_SYNC_COMPLETE, - SplitInternalEvent.SEGMENTS_SYNC_COMPLETE) + SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, + SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE) // SDK_READY_FROM_CACHE: OR of ANDs // Fires when (cache group all done) OR (sync group all done) diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java b/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java index 48d32bec3..3849e8e5b 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitInternalEvent.java @@ -1,33 +1,26 @@ package io.split.android.client.events; /** - * Created by sarrubia on 4/6/18. + * Internal events used to track SDK initialization and data updates. */ - public enum SplitInternalEvent { + // Cache loading events MY_SEGMENTS_LOADED_FROM_STORAGE, SPLITS_LOADED_FROM_STORAGE, - MY_SEGMENTS_FETCHED, - MY_SEGMENTS_UPDATED, - SPLITS_FETCHED, - SPLITS_UPDATED, - SDK_READY_TIMEOUT_REACHED, - SPLIT_KILLED_NOTIFICATION, ATTRIBUTES_LOADED_FROM_STORAGE, ENCRYPTION_MIGRATION_DONE, + + // Data update events (fired only when data actually changed) + MY_SEGMENTS_UPDATED, + SPLITS_UPDATED, MY_LARGE_SEGMENTS_UPDATED, RULE_BASED_SEGMENTS_UPDATED, + SPLIT_KILLED_NOTIFICATION, - /** - * Synthetic event: fired when splits sync completes (either SPLITS_FETCHED or SPLITS_UPDATED). - * Used internally to simplify SDK_READY and SDK_READY_FROM_CACHE condition evaluation. - */ - SPLITS_SYNC_COMPLETE, + // Sync completion events (fired when sync completes, regardless of data change) + TARGETING_RULES_SYNC_COMPLETE, + MEMBERSHIPS_SYNC_COMPLETE, - /** - * Synthetic event: fired when segments sync completes (any of MY_SEGMENTS_FETCHED, - * MY_SEGMENTS_UPDATED, or MY_LARGE_SEGMENTS_UPDATED). - * Used internally to simplify SDK_READY and SDK_READY_FROM_CACHE condition evaluation. - */ - SEGMENTS_SYNC_COMPLETE, + // Other events + SDK_READY_TIMEOUT_REACHED, } diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java index 9184d2810..05ac09f9f 100644 --- a/main/src/androidTest/java/tests/service/EventsManagerTest.java +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -13,7 +13,6 @@ import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutorImpl; -import io.split.android.client.service.synchronizer.ThreadUtils; public class EventsManagerTest { @Test @@ -37,7 +36,7 @@ public void testSdkUpdateSplits() throws InterruptedException { } @Test - public void testSdkFetchedUpdatedSplits() throws InterruptedException { + public void testSdkUpdateTriggersAfterReady() throws InterruptedException { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); @@ -47,8 +46,10 @@ public void testSdkFetchedUpdatedSplits() throws InterruptedException { TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + // First make SDK_READY fire by completing sync + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with a data change eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); updateLatch.await(5, TimeUnit.SECONDS); @@ -57,7 +58,7 @@ public void testSdkFetchedUpdatedSplits() throws InterruptedException { } @Test - public void testSdkUpdatedFetchedSplits() throws InterruptedException { + public void testSdkUpdateDoesNotTriggerBeforeReady() throws InterruptedException { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); @@ -67,12 +68,11 @@ public void testSdkUpdatedFetchedSplits() throws InterruptedException { TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + // Fire UPDATED before SDK_READY - should NOT trigger SDK_UPDATE due to prerequisite eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - updateLatch.await(5, TimeUnit.SECONDS); + updateLatch.await(2, TimeUnit.SECONDS); Assert.assertFalse(updateTask.onExecutedCalled); } @@ -98,7 +98,7 @@ public void testSdkUpdateSegments() throws InterruptedException { } @Test - public void testSdkFetchedUpdatedSegments() throws InterruptedException { + public void testSdkUpdateTriggersOnSegmentChange() throws InterruptedException { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); @@ -108,8 +108,10 @@ public void testSdkFetchedUpdatedSegments() throws InterruptedException { TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + // Make SDK_READY fire + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // Then trigger SDK_UPDATE with a segment change eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); updateLatch.await(5, TimeUnit.SECONDS); @@ -118,7 +120,7 @@ public void testSdkFetchedUpdatedSegments() throws InterruptedException { } @Test - public void testSdkUpdatedFetchedSegments() throws InterruptedException { + public void testSdkUpdateRequiresDataChange() throws InterruptedException { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); @@ -128,13 +130,14 @@ public void testSdkUpdatedFetchedSegments() throws InterruptedException { TestingHelper.TestEventTask updateTask = TestingHelper.testTask(updateLatch); eventManager.register(SplitEvent.SDK_UPDATE, updateTask); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + // Make SDK_READY fire with only SYNC_COMPLETE events (no UPDATED) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + // No UPDATED events fired + updateLatch.await(2, TimeUnit.SECONDS); - updateLatch.await(5, TimeUnit.SECONDS); - + // SDK_UPDATE should NOT fire because no data actually changed Assert.assertFalse(updateTask.onExecutedCalled); } diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java index 68e80dd28..9c769aaba 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java @@ -216,7 +216,7 @@ private void loadSplits() { } if (!content.equals(mLastContentLoaded)) { mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } mLastContentLoaded = content; diff --git a/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java b/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java index 2fd5cbded..cc33debfd 100644 --- a/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java +++ b/main/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java @@ -1,5 +1,7 @@ package io.split.android.client.localhost.shared; +import androidx.annotation.VisibleForTesting; + import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; @@ -31,6 +33,7 @@ public class LocalhostSplitClientContainerImpl extends BaseSplitClientContainer private final EventsManagerCoordinator mEventsManagerCoordinator; private final SplitTaskExecutor mSplitTaskExecutor; private final FlagSetsFilter mFlagSetsFilter; + private final SplitEventsManagerFactory mEventsManagerFactory; public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, SplitClientConfig config, @@ -42,6 +45,24 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, EventsManagerCoordinator eventsManagerCoordinator, SplitTaskExecutor taskExecutor, FlagSetsFilter flagSetsFilter) { + this(splitFactory, config, splitsStorage, splitParser, attributesManagerFactory, + attributesMerger, telemetryStorageProducer, eventsManagerCoordinator, + taskExecutor, flagSetsFilter, + new DefaultSplitEventsManagerFactory(taskExecutor, config)); + } + + @VisibleForTesting + LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, + SplitClientConfig config, + SplitsStorage splitsStorage, + SplitParser splitParser, + AttributesManagerFactory attributesManagerFactory, + AttributesMerger attributesMerger, + TelemetryStorageProducer telemetryStorageProducer, + EventsManagerCoordinator eventsManagerCoordinator, + SplitTaskExecutor taskExecutor, + FlagSetsFilter flagSetsFilter, + SplitEventsManagerFactory eventsManagerFactory) { mSplitFactory = splitFactory; mConfig = config; mSplitStorage = splitsStorage; @@ -52,13 +73,14 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, mEventsManagerCoordinator = eventsManagerCoordinator; mSplitTaskExecutor = taskExecutor; mFlagSetsFilter = flagSetsFilter; + mEventsManagerFactory = eventsManagerFactory; } @Override protected void createNewClient(Key key) { - SplitEventsManager eventsManager = new SplitEventsManager(mSplitTaskExecutor, mConfig.blockUntilReady()); + SplitEventsManager eventsManager = mEventsManagerFactory.create(); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); - eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + eventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); AttributesStorageImpl attributesStorage = new AttributesStorageImpl(); @@ -88,4 +110,19 @@ protected void createNewClient(Key key) { public void destroy() { // No-op } + + private static class DefaultSplitEventsManagerFactory implements SplitEventsManagerFactory { + private final SplitTaskExecutor mTaskExecutor; + private final int mBlockUntilReady; + + DefaultSplitEventsManagerFactory(SplitTaskExecutor taskExecutor, SplitClientConfig config) { + mTaskExecutor = taskExecutor; + mBlockUntilReady = config.blockUntilReady(); + } + + @Override + public SplitEventsManager create() { + return new SplitEventsManager(mTaskExecutor, mBlockUntilReady); + } + } } diff --git a/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java b/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java new file mode 100644 index 000000000..1dfb51404 --- /dev/null +++ b/main/src/main/java/io/split/android/client/localhost/shared/SplitEventsManagerFactory.java @@ -0,0 +1,17 @@ +package io.split.android.client.localhost.shared; + +import io.split.android.client.events.SplitEventsManager; + +/** + * Factory interface for creating SplitEventsManager instances. + * Package-local interface to allow testing by injecting mock implementations. + */ +interface SplitEventsManagerFactory { + /** + * Creates a new SplitEventsManager instance. + * + * @return a new SplitEventsManager instance + */ + SplitEventsManager create(); +} + diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index 2158985aa..e79e7b070 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -50,7 +50,6 @@ public class MySegmentsSyncTask implements SplitTask { private final SplitTaskType mTaskType; private final SplitInternalEvent mUpdateEvent; - private final SplitInternalEvent mFetchedEvent; private final OperationType mTelemetryOperationType; private final boolean mAvoidCache; @@ -105,7 +104,6 @@ public MySegmentsSyncTask(@NonNull HttpFetcher mySegmentsFetc mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); mTaskType = config.getTaskType(); mUpdateEvent = config.getUpdateEvent(); - mFetchedEvent = config.getFetchedEvent(); mTelemetryOperationType = config.getTelemetryOperationType(); mTargetSegmentsChangeNumber = targetSegmentsChangeNumber; mTargetLargeSegmentsChangeNumber = targetLargeSegmentsChangeNumber; @@ -265,28 +263,21 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, return; } - // MY_SEGMENTS_UPDATED event when segments have changed + // Always fire SEGMENTS_SYNC_COMPLETE when sync succeeds + mEventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + + // Check if data actually changed boolean segmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(segmentsResult.oldSegments, segmentsResult.newSegments); boolean largeSegmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); if (segmentsHaveChanged) { Logger.v("New segments: " + segmentsResult.newSegments); + mEventsManager.notifyInternalEvent(mUpdateEvent); } if (largeSegmentsHaveChanged) { Logger.v("New large segments: " + largeSegmentsResult.newSegments); - } - - if (segmentsHaveChanged) { - mEventsManager.notifyInternalEvent(mUpdateEvent); - } else { - // MY_LARGE_SEGMENTS_UPDATED event when large segments have changed - if (largeSegmentsHaveChanged) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); - } else { - // otherwise, MY_SEGMENTS_FETCHED event - mEventsManager.notifyInternalEvent(mFetchedEvent); - } + mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); } } diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java index 210fb2d4e..77ddd812d 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java @@ -11,20 +11,16 @@ public class MySegmentsSyncTaskConfig { private static final MySegmentsSyncTaskConfig MY_SEGMENTS_TASK_CONFIG = new MySegmentsSyncTaskConfig( SplitTaskType.MY_SEGMENTS_SYNC, SplitInternalEvent.MY_SEGMENTS_UPDATED, - SplitInternalEvent.MY_SEGMENTS_FETCHED, OperationType.MY_SEGMENT); private final SplitTaskType mTaskType; private final SplitInternalEvent mUpdateEvent; - private final SplitInternalEvent mFetchedEvent; private final OperationType mTelemetryOperationType; private MySegmentsSyncTaskConfig(@NonNull SplitTaskType taskType, @NonNull SplitInternalEvent updateEvent, - @NonNull SplitInternalEvent fetchedEvent, @NonNull OperationType telemetryOperationType) { mTaskType = taskType; mUpdateEvent = updateEvent; - mFetchedEvent = fetchedEvent; mTelemetryOperationType = telemetryOperationType; } @@ -36,10 +32,6 @@ SplitInternalEvent getUpdateEvent() { return mUpdateEvent; } - SplitInternalEvent getFetchedEvent() { - return mFetchedEvent; - } - OperationType getTelemetryOperationType() { return mTelemetryOperationType; } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 2cb35e578..834755c41 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -94,12 +94,13 @@ public SplitTaskExecutionInfo execute() { private void notifyInternalEvent(long storedChangeNumber) { if (mEventsManager != null) { - SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; + // Always fire SPLITS_SYNC_COMPLETE when sync succeeds + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + + // Fire SPLITS_UPDATED only if data actually changed if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill())) { - event = SplitInternalEvent.SPLITS_UPDATED; + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } - - mEventsManager.notifyInternalEvent(event); } } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index 8f0a7cf61..ca3138d43 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -67,12 +67,14 @@ public SplitTaskExecutionInfo execute() { SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(new SplitsSyncHelper.SinceChangeNumbers(mChangeNumber, mRbsChangeNumber), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { - SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; + // Always fire SPLITS_SYNC_COMPLETE when sync succeeds + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + + // Fire SPLITS_UPDATED only if data actually changed if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill()) || mChangeChecker.changeNumberIsNewer(storedRbsChangeNumber, mRuleBasedSegmentStorage.getChangeNumber())) { - event = SplitInternalEvent.SPLITS_UPDATED; + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } - mEventsManager.notifyInternalEvent(event); } return result; } diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index 60978714b..4f289c650 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -46,14 +46,14 @@ public void RULE_BASED_SEGMENTEventIsPassedDownToChildren() { } @Test - public void SPLITS_FETCHEDEventIsPassedDownToChildren() { + public void SPLITS_SYNC_COMPLETEEventIsPassedDownToChildren() { mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); delay(); - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); } @Test @@ -83,14 +83,14 @@ public void EventIsPassedDownToChildrenIfRegisteredAfterEmission() { ISplitEventsManager newMockChildEventsManager = mock(ISplitEventsManager.class); mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); delay(); - verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); mEventsManager.registerEventsManager(new Key("new_key", "bucketing"), newMockChildEventsManager); - verify(newMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + verify(newMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); } private void delay() { diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java index c72f2fddf..d4a1e1977 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -40,9 +40,10 @@ public void eventOnReady() { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + // Fire SYNC_COMPLETE events to trigger SDK_READY + // This also triggers SDK_READY_FROM_CACHE via the sync path (OR-of-ANDs) + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -84,10 +85,9 @@ public void eventOnReadyAndOnReadyTimedOut() { //At this line timeout has been reached assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); - //But if after timeout event, the Splits and MySegments are ready, SDK_READY should be triggered - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + //But if after timeout event, the sync completes, SDK_READY should be triggered + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); shouldStop = false; maxExecutionTime = System.currentTimeMillis() + 10000; @@ -181,8 +181,9 @@ public void sdkReadyWithSplitsAndUpdatedLargeSegments() { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), cfg.blockUntilReady()); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + // Fire SYNC_COMPLETE events to trigger SDK_READY + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; @@ -213,8 +214,8 @@ public void onPostExecutionView(SplitClient client) { } }); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); - eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + eventManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); boolean readyAwait = readyLatch.await(3, TimeUnit.SECONDS); eventManager.notifyInternalEvent(eventToCheck); diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java new file mode 100644 index 000000000..ebfbdd2e6 --- /dev/null +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java @@ -0,0 +1,82 @@ +package io.split.android.client.localhost; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.AssetManager; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import io.split.android.client.events.EventsManagerCoordinator; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.storage.legacy.FileStorage; + +public class LocalhostSplitsStorageTest { + + @Mock + private Context mContext; + @Mock + private AssetManager mAssetManager; + @Mock + private FileStorage mFileStorage; + @Mock + private EventsManagerCoordinator mEventsManagerCoordinator; + + private LocalhostSplitsStorage mLocalhostSplitsStorage; + private static final String TEST_FILE_NAME = "test-splits.yaml"; + private static final String INITIAL_CONTENT = "splits:\n - name: split1\n treatment: on"; + private static final String UPDATED_CONTENT = "splits:\n - name: split2\n treatment: off"; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.openMocks(this); + when(mContext.getAssets()).thenReturn(mAssetManager); + when(mAssetManager.open(anyString())).thenThrow(new FileNotFoundException("File not found in assets")); + when(mFileStorage.read(TEST_FILE_NAME)).thenReturn(INITIAL_CONTENT); + mLocalhostSplitsStorage = new LocalhostSplitsStorage(TEST_FILE_NAME, mContext, mFileStorage, mEventsManagerCoordinator); + } + + @Test + public void loadLocalNotifiesTargetingRulesSyncCompleteAndSplitsUpdatedWhenContentChanges() throws IOException { + // First load - should notify events (lines 219-220) + mLocalhostSplitsStorage.loadLocal(); + + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + + // Update content and reload + when(mFileStorage.read(TEST_FILE_NAME)).thenReturn(UPDATED_CONTENT); + mLocalhostSplitsStorage.loadLocal(); + + // Should notify events again since content changed + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(2)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(2)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + } + + @Test + public void loadLocalDoesNotNotifyEventsWhenContentUnchanged() throws IOException { + // First load - should notify events + mLocalhostSplitsStorage.loadLocal(); + + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + + // Reload with same content - should NOT notify events again + mLocalhostSplitsStorage.loadLocal(); + + // Verify events were only called once + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManagerCoordinator, org.mockito.Mockito.times(1)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + } +} + diff --git a/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java b/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java index 2088b392a..05b04cc91 100644 --- a/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java +++ b/main/src/test/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImplTest.java @@ -27,6 +27,9 @@ import io.split.android.client.attributes.AttributesManagerFactory; import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.events.EventsManagerCoordinator; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.client.localhost.LocalhostSplitFactory; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.storage.splits.SplitsStorage; @@ -98,6 +101,40 @@ public void gettingNewClientRegistersEventManager() { verify(mEventsManagerCoordinator).registerEventsManager(eq(key), any()); } + @Test + public void gettingNewClientNotifiesInternalEvents() { + // Create a mocked SplitEventsManager + SplitEventsManager mockEventsManager = mock(SplitEventsManager.class); + SplitEventExecutorResources mockExecutorResources = mock(SplitEventExecutorResources.class); + when(mockEventsManager.getExecutorResources()).thenReturn(mockExecutorResources); + + // Create a mocked factory that returns the mocked events manager + SplitEventsManagerFactory mockFactory = () -> mockEventsManager; + + // Create client container with the mocked factory using @VisibleForTesting constructor + LocalhostSplitClientContainerImpl clientContainer = new LocalhostSplitClientContainerImpl( + mFactory, + mConfig, + mSplitsStorage, + mSplitParser, + mAttributesManagerFactory, + mAttributesMerger, + mTelemetryStorageProducer, + mEventsManagerCoordinator, + mTaskExecutor, + mFlagSetsFilter, + mockFactory + ); + + Key key = new Key("matching_key", "bucketing_key"); + clientContainer.getClient(key); + + // Verify that notifyInternalEvent is called on the mocked events manager + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + verify(mockEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + } + @NonNull private LocalhostSplitClientContainerImpl getClientContainer() { return new LocalhostSplitClientContainerImpl(mFactory, diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java index 696c844f0..4651eaa9a 100644 --- a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java @@ -222,14 +222,39 @@ public void addTillParameterToRequestWhenResponseCnDoesNotMatchTargetAndRetryLim } @Test - public void fetchedEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { + public void syncCompleteEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); + verify(mEventsManager, never()).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + } + + @Test + public void membershipsSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFetcherException { + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + // Verify MEMBERSHIPS_SYNC_COMPLETE is always fired on successful sync, even when segments changed + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED)); + } + + @Test + public void updateEventIsFiredWhenSegmentsHaveChanged() throws HttpFetcherException { + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED)); } @Test diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 9a4eb8580..513a500f3 100644 --- a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -125,10 +125,8 @@ public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherExcept @Test public void splitUpdatedNotified() throws HttpFetcherException { - // Check that syncing is done with changeNum retrieved from db - // Querystring is the same, so no clear sould be called - // And updateTimestamp is 0 - // Retry is off, so splitSyncHelper.sync should be called + // Check that both SPLITS_SYNC_COMPLETE and SPLITS_UPDATED are notified + // when sync completes with data changes mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); @@ -138,15 +136,14 @@ public void splitUpdatedNotified() throws HttpFetcherException { mTask.execute(); + verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } @Test - public void splitFetchdNotified() throws HttpFetcherException { - // Check that syncing is done with changeNum retrieved from db - // Querystring is the same, so no clear sould be called - // And updateTimestamp is 0 - // Retry is off, so splitSyncHelper.sync should be called + public void splitSyncCompleteNotifiedWhenNoDataChange() throws HttpFetcherException { + // Check that SPLITS_SYNC_COMPLETE is notified when sync completes + // but no data changes (SPLITS_UPDATED should NOT be notified) mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); @@ -156,7 +153,8 @@ public void splitFetchdNotified() throws HttpFetcherException { mTask.execute(); - verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + verify(mEventsManager, times(1)).notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + verify(mEventsManager, never()).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } @Test @@ -186,6 +184,51 @@ public void recordSuccessInTelemetry() { verify(mTelemetryRuntimeProducer).recordSuccessfulSync(eq(OperationType.SPLITS), longThat(arg -> arg > 0)); } + @Test + public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + + @Test + public void splitsUpdatedIsFiredWhenDataChanged() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + } + + @Test + public void splitsUpdatedIsNotFiredWhenDataUnchanged() throws HttpFetcherException { + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + @After public void tearDown() { reset(mSplitsStorage); diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 677030c0c..ca19d54e1 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -17,11 +17,14 @@ import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; 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.http.HttpFetcherException; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsUpdateTask; +import io.split.android.client.service.synchronizer.SplitsChangeChecker; import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.helpers.FileHelper; @@ -76,7 +79,7 @@ public void storedChangeNumBigger() throws HttpFetcherException { } @Test - public void storedRbsChangeNumBigger() throws HttpFetcherException { + public void storedRbsChangeNumBigger() { when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(mRbsChangeNumber + 100L); mTask.execute(); @@ -84,6 +87,61 @@ public void storedRbsChangeNumBigger() throws HttpFetcherException { verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); } + @Test + public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() { + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + + @Test + public void splitsUpdatedIsFiredWhenSplitsDataChanged() { + long storedChangeNumber = 100L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber).thenReturn(150L); // After sync, change number increased + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + + @Test + public void splitsUpdatedIsFiredWhenRbsDataChanged() { + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber).thenReturn(250L); // After sync, RBS change number increased + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + + @Test + public void splitsUpdatedIsNotFiredWhenDataUnchanged() { + long storedChangeNumber = 100L; + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber); // Same before and after sync + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber); // Same before and after sync + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + + mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + } + @After public void tearDown() { reset(mSplitsStorage); diff --git a/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java index 07f4fa6a0..7e2d8b3c0 100644 --- a/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java +++ b/main/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java @@ -16,7 +16,6 @@ public void getForMySegments() { assertEquals(config.getTaskType(), SplitTaskType.MY_SEGMENTS_SYNC); assertEquals(config.getUpdateEvent(), SplitInternalEvent.MY_SEGMENTS_UPDATED); - assertEquals(config.getFetchedEvent(), SplitInternalEvent.MY_SEGMENTS_FETCHED); assertEquals(config.getTelemetryOperationType(), OperationType.MY_SEGMENT); } }