Skip to content

Commit dd21578

Browse files
author
AR Abdul Azeez
committed
fix: resolve pre-existing test failures on main
- SDKInitTests: make initWithContext(context, appId) non-blocking by always dispatching internalInit asynchronously. Update getServiceWithFeatureGate to properly handle FAILED (throw initFailureException) and IN_PROGRESS (block until ready) states instead of only checking isInitialized. Add requireInitForOperation helper for login/logout to also wait when init is in progress. - InAppMessagesManagerTests: add suspendifyOnMain mock to IOMockHelper so fireOnMain callbacks execute synchronously in tests, eliminating the race condition where verify ran before the spawned thread. - FeatureManagerTests: add afterEach cleanup for ThreadingMode global state to prevent CI flakiness from parallel test execution. Made-with: Cursor
1 parent 6103044 commit dd21578

File tree

3 files changed

+48
-28
lines changed

3 files changed

+48
-28
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ internal class OneSignalImp(
5050
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
5151
) : IOneSignal, IServiceProvider {
5252

53-
private val suspendCompletion = CompletableDeferred<Unit>()
53+
@Volatile
54+
private var suspendCompletion = CompletableDeferred<Unit>()
5455

5556
@Volatile
5657
private var initState: InitState = InitState.NOT_STARTED
@@ -288,34 +289,30 @@ internal class OneSignalImp(
288289
context: Context,
289290
appId: String,
290291
): Boolean {
291-
Logging.log(LogLevel.DEBUG, "Calling deprecated initWithContextSuspend(context: $context, appId: $appId)")
292+
Logging.log(LogLevel.DEBUG, "initWithContext(context: $context, appId: $appId)")
292293

293-
// do not do this again if already initialized or init is in progress
294294
synchronized(initLock) {
295295
if (initState.isSDKAccessible()) {
296296
Logging.log(LogLevel.DEBUG, "initWithContext: SDK already initialized or in progress")
297297
return true
298298
}
299299

300300
initState = InitState.IN_PROGRESS
301+
suspendCompletion = CompletableDeferred()
301302
}
302303

304+
initFailureException = IllegalStateException("OneSignal initWithContext failed.")
305+
303306
// FeatureManager depends on ConfigModelStore/PreferencesService which requires appContext.
304307
// Ensure app context is available before evaluating feature gates.
305308
ensureApplicationServiceStarted(context)
306309

307-
if (isBackgroundThreadingEnabled) {
308-
// init in background and return immediately to ensure non-blocking
309-
suspendifyOnIO {
310-
internalInit(context, appId)
311-
}
312-
return true
313-
}
314-
315-
// Legacy FF-OFF behavior intentionally blocks caller thread until initialization completes.
316-
return runBlocking(runtimeIoDispatcher) {
310+
// Always dispatch init asynchronously so this method never blocks the caller.
311+
// Callers that need to wait (accessors, login, logout) will block via suspendCompletion.
312+
suspendifyOnIO {
317313
internalInit(context, appId)
318314
}
315+
return true
319316
}
320317

321318
/**
@@ -380,9 +377,7 @@ internal class OneSignalImp(
380377
waitForInit(operationName = "login")
381378
suspendifyOnIO { loginHelper.login(externalId, jwtBearerToken) }
382379
} else {
383-
if (!isInitialized) {
384-
throw IllegalStateException("Must call 'initWithContext' before 'login'")
385-
}
380+
requireInitForOperation("login")
386381
Thread {
387382
runBlocking(runtimeIoDispatcher) {
388383
loginHelper.login(externalId, jwtBearerToken)
@@ -398,9 +393,7 @@ internal class OneSignalImp(
398393
waitForInit(operationName = "logout")
399394
suspendifyOnIO { logoutHelper.logout() }
400395
} else {
401-
if (!isInitialized) {
402-
throw IllegalStateException("Must call 'initWithContext' before 'logout'")
403-
}
396+
requireInitForOperation("logout")
404397
Thread {
405398
runBlocking(runtimeIoDispatcher) {
406399
logoutHelper.logout()
@@ -417,6 +410,22 @@ internal class OneSignalImp(
417410

418411
override fun <T> getAllServices(c: Class<T>): List<T> = services.getAllServices(c)
419412

413+
/**
414+
* Ensures initialization is complete before proceeding with an operation.
415+
* Blocks if init is in progress; throws immediately if not started or failed.
416+
*/
417+
private fun requireInitForOperation(operationName: String) {
418+
when (initState) {
419+
InitState.NOT_STARTED ->
420+
throw IllegalStateException("Must call 'initWithContext' before '$operationName'")
421+
InitState.IN_PROGRESS -> waitForInit(operationName = operationName)
422+
InitState.FAILED ->
423+
throw initFailureException
424+
?: IllegalStateException("Initialization failed before '$operationName'")
425+
InitState.SUCCESS -> {}
426+
}
427+
}
428+
420429
/**
421430
* Blocking version that waits for initialization to complete.
422431
* Uses runBlocking to bridge to the suspend implementation.
@@ -514,14 +523,15 @@ internal class OneSignalImp(
514523
}
515524

516525
private fun <T> getServiceWithFeatureGate(getter: () -> T): T {
517-
return if (isBackgroundThreadingEnabled) {
518-
waitAndReturn(getter)
519-
} else {
520-
if (isInitialized) {
521-
getter()
522-
} else {
523-
throw IllegalStateException("Must call 'initWithContext' before use")
524-
}
526+
if (isBackgroundThreadingEnabled) {
527+
return waitAndReturn(getter)
528+
}
529+
return when (initState) {
530+
InitState.SUCCESS -> getter()
531+
InitState.IN_PROGRESS -> waitAndReturn(getter)
532+
InitState.FAILED -> throw initFailureException
533+
?: IllegalStateException("Initialization failed. Cannot proceed.")
534+
InitState.NOT_STARTED -> throw IllegalStateException("Must call 'initWithContext' before use")
525535
}
526536
}
527537

@@ -626,10 +636,10 @@ internal class OneSignalImp(
626636
}
627637

628638
initState = InitState.IN_PROGRESS
639+
suspendCompletion = CompletableDeferred()
629640
}
630641

631642
val result = internalInit(context, appId)
632-
// initState is already set correctly in internalInit, no need to overwrite it
633643
result
634644
}
635645
}

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/features/FeatureManagerTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class FeatureManagerTests : FunSpec({
1616
ThreadingMode.useBackgroundThreading = false
1717
}
1818

19+
afterEach {
20+
ThreadingMode.useBackgroundThreading = false
21+
}
22+
1923
test("initial state should enable BACKGROUND_THREADING when feature is present") {
2024
// Given
2125
val initialModel = mockk<ConfigModel>()

OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/IOMockHelper.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.onesignal.mocks
22

33
import com.onesignal.common.threading.OneSignalDispatchers
44
import com.onesignal.common.threading.suspendifyOnIO
5+
import com.onesignal.common.threading.suspendifyOnMain
56
import io.kotest.core.listeners.AfterSpecListener
67
import io.kotest.core.listeners.BeforeSpecListener
78
import io.kotest.core.listeners.BeforeTestListener
@@ -114,6 +115,11 @@ object IOMockHelper : BeforeSpecListener, AfterSpecListener, BeforeTestListener,
114115
trackAsyncWork(block)
115116
}
116117

118+
every { suspendifyOnMain(any<suspend () -> Unit>()) } answers {
119+
val block = firstArg<suspend () -> Unit>()
120+
trackAsyncWork(block)
121+
}
122+
117123
every { OneSignalDispatchers.launchOnIO(any<suspend () -> Unit>()) } answers {
118124
val block = firstArg<suspend () -> Unit>()
119125
trackAsyncWork(block)

0 commit comments

Comments
 (0)