Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
46e736d
Move 13 criteria tests from androidTest to unit tests
franco-zalamena-iterable Mar 24, 2026
2426fb7
Replace non-deterministic test synchronization with looper control
franco-zalamena-iterable Mar 24, 2026
69acb4a
Document root causes in @Ignore annotations
franco-zalamena-iterable Mar 24, 2026
0a823b6
Fix for deprecated test sleep
franco-zalamena-iterable Mar 24, 2026
e3c36f0
Fix flaky testSyncOnLogin and testTrackPushOpenWithCustomAction
franco-zalamena-iterable Mar 25, 2026
9abed24
Fix flaky testCriteriaMetEmailMergeTrue in IterableApiMergeUserEmailT…
franco-zalamena-iterable Mar 25, 2026
a218b09
Fix flaky push registration and InAppManager read state tests
franco-zalamena-iterable Mar 25, 2026
d97ce0c
Fix flaky InAppManager tests: listener, action link, and custom actio…
franco-zalamena-iterable Mar 25, 2026
93cdb30
Fix flaky InAppManager tests by draining setUp HTTP requests
franco-zalamena-iterable Mar 25, 2026
567e738
Fix flaky PII masking tests with deterministic init state
franco-zalamena-iterable Mar 25, 2026
a217a65
Fix flaky push registration test and increase async init timeouts
franco-zalamena-iterable Mar 26, 2026
933171f
Fix executor shutdown race in PII masking tests
franco-zalamena-iterable Mar 26, 2026
86f560a
Fix executor shutdown race between tests
franco-zalamena-iterable Mar 30, 2026
f8b38ac
Read in lock but execute without it
franco-zalamena-iterable Apr 6, 2026
f92fe0e
Merge branch 'master' into SDK-fix-flaky-tests
sumeruchat Apr 8, 2026
8ebbb16
Passing ExecutorService as parameter instead of lock read
franco-zalamena-iterable Apr 8, 2026
9a74970
Drains the initialize requests before tests
franco-zalamena-iterable Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ void processAll(ExecutorService executor) {
}
isProcessing = false;

// After processing all operations, shut down the executor
// After processing all operations, shut down the executor.
// Pass the executor reference explicitly so a concurrent reset()
// cannot swap in a new one that we accidentally shut down.
IterableLogger.d(TAG, "All queued operations processed, shutting down background executor");
shutdownBackgroundExecutorAsync();
shutdownBackgroundExecutorAsync(backgroundExecutor);
});
}

Expand Down Expand Up @@ -334,28 +336,27 @@ static void shutdownBackgroundExecutor() {
}

/**
* Shutdown the background executor asynchronously to avoid blocking the executor thread itself
* Used internally after initialization completes
* Shutdown the given executor asynchronously to avoid blocking the executor thread itself.
* The caller passes the exact executor instance that should be shut down, so a concurrent
* reset() that swaps in a new executor cannot cause us to shut down the wrong one.
*/
private static void shutdownBackgroundExecutorAsync() {
// Schedule shutdown on a separate thread to avoid blocking the executor thread
private static void shutdownBackgroundExecutorAsync(ExecutorService executorToShutdown) {
if (executorToShutdown == null || executorToShutdown.isShutdown()) {
return;
}
new Thread(() -> {
synchronized (initLock) {
if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) {
backgroundExecutor.shutdown();
try {
if (!backgroundExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
backgroundExecutor.shutdownNow();
}
} catch (InterruptedException e) {
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
backgroundExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
IterableLogger.d(TAG, "Background executor shutdown completed");
try {
executorToShutdown.shutdown();
if (!executorToShutdown.awaitTermination(5, TimeUnit.SECONDS)) {
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
executorToShutdown.shutdownNow();
}
} catch (InterruptedException e) {
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
executorToShutdown.shutdownNow();
Thread.currentThread().interrupt();
}
IterableLogger.d(TAG, "Background executor shutdown completed");
}, "IterableExecutorShutdown").start();
}

Expand Down Expand Up @@ -413,9 +414,13 @@ static void resetBackgroundInitializationState() {
pendingCallbacks.clear();
callbackManager.reset();

// Recreate executor if it was shut down
if (backgroundExecutor == null || backgroundExecutor.isShutdown()) {
backgroundExecutor = createExecutor();
// Swap in a fresh executor first, then shut down the old one.
// This ensures shutdownBackgroundExecutorAsync (which may still be
// pending from the old executor) cannot kill the new one.
ExecutorService oldExecutor = backgroundExecutor;
backgroundExecutor = createExecutor();
if (oldExecutor != null && !oldExecutor.isShutdown()) {
oldExecutor.shutdownNow();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ private void reInitIterableApi() {
authHandler = mock(IterableAuthHandler.class);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testRefreshToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand Down Expand Up @@ -95,7 +95,7 @@ public void testRefreshToken() throws Exception {
timer = IterableApi.getInstance().getAuthManager().timer;
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetEmailWithToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -119,7 +119,7 @@ public void testSetEmailWithToken() throws Exception {
shadowOf(getMainLooper()).runToEndOfTasks();
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetEmailWithTokenExpired() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -133,7 +133,7 @@ public void testSetEmailWithTokenExpired() throws Exception {
assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetUserIdWithToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -157,7 +157,7 @@ public void testSetUserIdWithToken() throws Exception {
assertEquals(expiredJWT, IterableApi.getInstance().getAuthToken());
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSameEmailWithNewToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -181,7 +181,7 @@ public void testSameEmailWithNewToken() throws Exception {
assertEquals(IterableApi.getInstance().getAuthToken(), newJWT);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSameUserIdWithNewToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -200,7 +200,7 @@ public void testSameUserIdWithNewToken() throws Exception {
assertEquals(IterableApi.getInstance().getAuthToken(), newJWT);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetSameEmailAndRemoveToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -219,7 +219,7 @@ public void testSetSameEmailAndRemoveToken() throws Exception {
assertNull(IterableApi.getInstance().getAuthToken());
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetSameUserIdAndRemoveToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand Down Expand Up @@ -277,7 +277,7 @@ public void testSetSameUserId() throws Exception {
assertNull(IterableApi.getInstance().getAuthToken());
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetSameEmailWithSameToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand All @@ -297,7 +297,7 @@ public void testSetSameEmailWithSameToken() throws Exception {
assertEquals(IterableApi.getInstance().getAuthToken(), token);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testSetSameUserIdWithSameToken() throws Exception {
IterableApi.initialize(getContext(), "apiKey");
Expand Down Expand Up @@ -352,7 +352,7 @@ public void testUserIdLogOut() throws Exception {
assertNull(IterableApi.getInstance().getAuthToken());
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthTokenPresentInRequest() throws Exception {
// server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
Expand Down Expand Up @@ -392,7 +392,7 @@ public void testAuthTokenPresentInRequest() throws Exception {
assertEquals(HEADER_SDK_AUTH_FORMAT + newJWT, getMessagesSet2Request.getHeader("Authorization"));
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthFailureReturns401() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand All @@ -418,7 +418,7 @@ public void testAuthFailureReturns401() throws InterruptedException {
assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthRequestedOnSetEmail() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand All @@ -433,7 +433,7 @@ public void testAuthRequestedOnSetEmail() throws InterruptedException {

}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthRequestedOnUpdateEmail() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand All @@ -447,7 +447,7 @@ public void testAuthRequestedOnUpdateEmail() throws InterruptedException {
//TODO: Shouldn't the update call also update the authToken in IterableAPI class?
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthRequestedOnSetUserId() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand All @@ -456,7 +456,7 @@ public void testAuthRequestedOnSetUserId() throws InterruptedException {
assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT);
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testAuthSetToNullOnLogOut() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand All @@ -469,7 +469,7 @@ public void testAuthSetToNullOnLogOut() throws InterruptedException {
assertNull(IterableApi.getInstance().getAuthToken());
}

@Ignore ("Ignoring the JWT Tests")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testRegisterForPushInvokedAfterTokenRefresh() throws InterruptedException {
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ private void addResponse(String endPoint) {
dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
}

/**
* Takes the next request matching the expected endpoint, skipping any spurious
* in-app sync requests caused by cross-test state leakage.
*/
private RecordedRequest takeRequestWithPath(String expectedEndpoint) throws InterruptedException {
String expectedPath = "/" + expectedEndpoint;
RecordedRequest request;
do {
request = server.takeRequest(1, TimeUnit.SECONDS);
if (request == null) return null;
} while (request.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES)
&& !expectedPath.startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
return request;
}

// all userId tests
@Test
public void testCriteriaNotMetUserIdDefault() throws Exception {
Expand Down Expand Up @@ -844,18 +859,18 @@ public void testCriteriaMetEmailMergeTrue() throws Exception {
triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
shadowOf(getMainLooper()).idle();

// check if request was sent to unknown user session endpoint
RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
// check if request was sent to unknown user session endpoint (skip any spurious in-app syncs)
RecordedRequest unknownSessionRequest = takeRequestWithPath(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());

// check if request was sent to track purchase endpoint
RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
RecordedRequest purchaseRequest = takeRequestWithPath(IterableConstants.ENDPOINT_TRACK_PURCHASE);
assertNotNull("Purchase request should not be null", purchaseRequest);
assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());

// check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
RecordedRequest inAppRequest = takeRequestWithPath(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
assertNotNull("InApp messages request should be sent", inAppRequest);
assertTrue("InApp messages request path should start with correct endpoint",
inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
Expand All @@ -872,7 +887,7 @@ public void testCriteriaMetEmailMergeTrue() throws Exception {
IterableApi.getInstance().setEmail(email, identityResolution);

// check if request was sent to merge endpoint
RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
RecordedRequest mergeRequest = takeRequestWithPath(IterableConstants.ENDPOINT_MERGE_USER);
assertNotNull(mergeRequest);
assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public void testPostRequestHeaders() throws Exception {
Assert.assertEquals("fake_key", request.getHeader(IterableConstants.HEADER_API_KEY));
}

@Ignore("Ignoring the JWT related test error")
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
@Test
public void testUpdateEmailRequest() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public void testUpdateEmailWithUserId() throws Exception {
assertEquals("testUserId", IterableApi.getInstance().getUserId());
}

@Ignore
@Ignore("handleAppLink performs real HTTP redirect - needs MockWebServer to stub the redirect endpoint")
@Test
public void testHandleUniversalLinkRewrite() throws Exception {
IterableUrlHandler urlHandlerMock = mock(IterableUrlHandler.class);
Expand All @@ -262,6 +262,9 @@ public void testHandleUniversalLinkRewrite() throws Exception {
@Test
public void testSetEmailWithAutomaticPushRegistration() throws Exception {
IterableApi.initialize(getContext(), "fake_key", new IterableConfig.Builder().setPushIntegrationName("pushIntegration").setAutoPushRegistration(true).build());
// Flush any pending looper callbacks from initialize, then reset mock
shadowOf(getMainLooper()).idle();
Mockito.reset(IterablePushRegistration.instance);

// Check that setEmail calls registerForPush
IterableApi.getInstance().setEmail("test@email.com");
Expand Down Expand Up @@ -290,6 +293,8 @@ public void testSetEmailWithoutAutomaticPushRegistration() throws Exception {
@Test
public void testSetUserIdWithAutomaticPushRegistration() throws Exception {
IterableApi.initialize(getContext(), "fake_key", new IterableConfig.Builder().setPushIntegrationName("pushIntegration").setAutoPushRegistration(true).build());
// Reset after initialize since it may trigger push registration via background init
Mockito.reset(IterablePushRegistration.instance);

// Check that setUserId calls registerForPush
IterableApi.getInstance().setUserId("userId");
Expand Down Expand Up @@ -423,7 +428,7 @@ public void testInAppResetOnLogout() throws Exception {
verify(IterableApi.sharedInstance.getInAppManager(), times(2)).reset();
}

@Ignore("Ignoring this test as it fails on CI for some reason")
@Ignore("Fails on CI: likely IterableTaskStorage singleton state leakage between tests - needs investigation")
@Test
public void databaseClearOnLogout() throws Exception {
IterableTaskStorage taskStorage = IterableTaskStorage.sharedInstance(getContext());
Expand Down
Loading
Loading