Skip to content

Commit 440b44d

Browse files
franco-zalamena-iterableclaudesumeruchat
authored
[SDK-406] Sdk fix flaky tests (#1007)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: sumeruchat <sumeru.chatterjee@iterable.com>
1 parent c42e6bc commit 440b44d

25 files changed

Lines changed: 146 additions & 124 deletions

iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ void processAll(ExecutorService executor) {
7474
}
7575
isProcessing = false;
7676

77-
// After processing all operations, shut down the executor
7877
IterableLogger.d(TAG, "All queued operations processed, shutting down background executor");
79-
shutdownBackgroundExecutorAsync();
78+
shutdownBackgroundExecutorAsync(executor);
8079
});
8180
}
8281

@@ -334,28 +333,27 @@ static void shutdownBackgroundExecutor() {
334333
}
335334

336335
/**
337-
* Shutdown the background executor asynchronously to avoid blocking the executor thread itself
338-
* Used internally after initialization completes
336+
* Shutdown the given executor asynchronously to avoid blocking the executor thread itself.
337+
* The caller passes the exact executor instance that should be shut down, so a concurrent
338+
* reset() that swaps in a new executor cannot cause us to shut down the wrong one.
339339
*/
340-
private static void shutdownBackgroundExecutorAsync() {
341-
// Schedule shutdown on a separate thread to avoid blocking the executor thread
340+
private static void shutdownBackgroundExecutorAsync(ExecutorService executorToShutdown) {
341+
if (executorToShutdown == null || executorToShutdown.isShutdown()) {
342+
return;
343+
}
342344
new Thread(() -> {
343-
synchronized (initLock) {
344-
if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) {
345-
backgroundExecutor.shutdown();
346-
try {
347-
if (!backgroundExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
348-
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
349-
backgroundExecutor.shutdownNow();
350-
}
351-
} catch (InterruptedException e) {
352-
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
353-
backgroundExecutor.shutdownNow();
354-
Thread.currentThread().interrupt();
355-
}
356-
IterableLogger.d(TAG, "Background executor shutdown completed");
345+
try {
346+
executorToShutdown.shutdown();
347+
if (!executorToShutdown.awaitTermination(5, TimeUnit.SECONDS)) {
348+
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
349+
executorToShutdown.shutdownNow();
357350
}
351+
} catch (InterruptedException e) {
352+
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
353+
executorToShutdown.shutdownNow();
354+
Thread.currentThread().interrupt();
358355
}
356+
IterableLogger.d(TAG, "Background executor shutdown completed");
359357
}, "IterableExecutorShutdown").start();
360358
}
361359

@@ -413,9 +411,13 @@ static void resetBackgroundInitializationState() {
413411
pendingCallbacks.clear();
414412
callbackManager.reset();
415413

416-
// Recreate executor if it was shut down
417-
if (backgroundExecutor == null || backgroundExecutor.isShutdown()) {
418-
backgroundExecutor = createExecutor();
414+
// Swap in a fresh executor first, then shut down the old one.
415+
// This ensures shutdownBackgroundExecutorAsync (which may still be
416+
// pending from the old executor) cannot kill the new one.
417+
ExecutorService oldExecutor = backgroundExecutor;
418+
backgroundExecutor = createExecutor();
419+
if (oldExecutor != null && !oldExecutor.isShutdown()) {
420+
oldExecutor.shutdownNow();
419421
}
420422
}
421423
}

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private void reInitIterableApi() {
6767
authHandler = mock(IterableAuthHandler.class);
6868
}
6969

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

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

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

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

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

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

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

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

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

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

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

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

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

434434
}
435435

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

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

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

472-
@Ignore ("Ignoring the JWT Tests")
472+
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
473473
@Test
474474
public void testRegisterForPushInvokedAfterTokenRefresh() throws InterruptedException {
475475
doReturn(expiredJWT).when(authHandler).onAuthTokenRequested();

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ private void addResponse(String endPoint) {
181181
dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
182182
}
183183

184+
/**
185+
* Takes the next request matching the expected endpoint, skipping any spurious
186+
* in-app sync requests caused by cross-test state leakage.
187+
*/
188+
private RecordedRequest takeRequestWithPath(String expectedEndpoint) throws InterruptedException {
189+
String expectedPath = "/" + expectedEndpoint;
190+
RecordedRequest request;
191+
do {
192+
request = server.takeRequest(1, TimeUnit.SECONDS);
193+
if (request == null) return null;
194+
} while (request.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES)
195+
&& !expectedPath.startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
196+
return request;
197+
}
198+
184199
// all userId tests
185200
@Test
186201
public void testCriteriaNotMetUserIdDefault() throws Exception {
@@ -844,18 +859,18 @@ public void testCriteriaMetEmailMergeTrue() throws Exception {
844859
triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
845860
shadowOf(getMainLooper()).idle();
846861

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

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

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

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

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public void testPostRequestHeaders() throws Exception {
225225
Assert.assertEquals("fake_key", request.getHeader(IterableConstants.HEADER_API_KEY));
226226
}
227227

228-
@Ignore("Ignoring the JWT related test error")
228+
@Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread")
229229
@Test
230230
public void testUpdateEmailRequest() throws Exception {
231231
server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));

iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public void testUpdateEmailWithUserId() throws Exception {
238238
assertEquals("testUserId", IterableApi.getInstance().getUserId());
239239
}
240240

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

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

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

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

0 commit comments

Comments
 (0)