diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index b1ab9ec2..26cc2595 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -4,6 +4,7 @@ import static com.launchdarkly.sdk.android.TestUtil.requireValue; import static org.easymock.EasyMock.anyBoolean; import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.checkOrder; import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -1461,19 +1462,27 @@ public void fdv2_rapidStateChangesCoalesceIntoOneRebuild() throws Exception { verifyForegroundDataSourceWasCreatedAndStarted(CONTEXT); verifyAll(); - // After startup, the rapid connectivity changes call updateEventProcessor in - // parallel threads, so ordering is nondeterministic. Use anyTimes(). + // After startup, the rapid connectivity changes call updateEventProcessor, which + // invokes setOffline then setInBackground once per change. The number of changes is + // nondeterministic, so we use anyTimes(). We also disable order checking on this + // strict mock: each updateEventProcessor call repeats the (setOffline, setInBackground) + // pair, which a strict mock would otherwise reject as out-of-order once the first + // setInBackground has been consumed. resetAll(); + checkOrder(eventProcessor, false); eventProcessor.setOffline(anyBoolean()); expectLastCall().anyTimes(); eventProcessor.setInBackground(anyBoolean()); expectLastCall().anyTimes(); replayAll(); - // Fire multiple rapid connectivity changes — debounce should coalesce them - mockPlatformState.setAndNotifyConnectivityChangeListeners(false); - mockPlatformState.setAndNotifyConnectivityChangeListeners(true); - mockPlatformState.setAndNotifyConnectivityChangeListeners(false); + // Fire multiple rapid connectivity changes — debounce should coalesce them. + // Deliver the burst on a single thread so the listener observes the values in a + // deterministic order ending on `false`. Three separate setAndNotify... calls each + // spawn their own thread, which can reorder so that `true` (the baseline) is applied + // last; the debouncer's A→B→C→A dedup would then suppress the rebuild and no data + // source would ever be stopped, flaking verifyDataSourceWasStopped() below. + mockPlatformState.setAndNotifyConnectivityChangeListenersInSequence(false, true, false); // Should result in exactly one data source rebuild (to OFFLINE) verifyDataSourceWasStopped(); diff --git a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockPlatformState.java b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockPlatformState.java index c08379a8..1b08ef95 100644 --- a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockPlatformState.java +++ b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockPlatformState.java @@ -72,6 +72,25 @@ public void setAndNotifyConnectivityChangeListeners(boolean networkAvailable) { }).start(); } + /** + * Notifies connectivity-change listeners with each value in {@code values}, in order, on a + * single background thread. Unlike repeated {@link #setAndNotifyConnectivityChangeListeners} + * calls — which each spawn their own thread and can therefore deliver notifications out of + * order — this delivers the whole sequence deterministically. The last value becomes the + * reported network state. Use this when a test depends on the final state after a burst of + * rapid changes (e.g. debounce coalescing). + */ + public void setAndNotifyConnectivityChangeListenersInSequence(boolean... values) { + new Thread(() -> { + for (boolean value : values) { + this.networkAvailable = value; + for (ConnectivityChangeListener listener : connectivityChangeListeners) { + listener.onConnectivityChanged(value); + } + } + }).start(); + } + public void setAndNotifyForegroundChangeListeners(boolean foreground) { this.foreground = foreground; new Thread(() -> {