Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package com.onesignal.core.internal.startup

import com.onesignal.common.services.ServiceProvider
import com.onesignal.common.threading.CoroutineDispatcherProvider
import com.onesignal.common.threading.DefaultDispatcherProvider
import com.onesignal.common.threading.OneSignalDispatchers

internal class StartupService(
private val services: ServiceProvider,
private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(),
) {
fun bootstrap() {
services.getAllServices<IBootstrapService>().forEach { it.bootstrap() }
}

// schedule to start all startable services using the provided dispatcher
// schedule to start all startable services using OneSignal dispatcher
fun scheduleStart() {
dispatchers.launchOnDefault {
OneSignalDispatchers.launchOnDefault {
services.getAllServices<IStartableService>().forEach { it.start() }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.onesignal.session.internal.outcomes.impl

import android.content.ContentValues
import com.onesignal.common.threading.OneSignalDispatchers
import com.onesignal.core.internal.database.IDatabaseProvider
import com.onesignal.core.internal.database.impl.OneSignalDbContract
import com.onesignal.debug.internal.logging.Logging
Expand All @@ -10,21 +9,20 @@ import com.onesignal.session.internal.influence.InfluenceChannel
import com.onesignal.session.internal.influence.InfluenceType
import com.onesignal.session.internal.influence.InfluenceType.Companion.fromString
import com.onesignal.session.internal.outcomes.migrations.RemoveInvalidSessionTimeRecords
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import java.util.Locale

internal class OutcomeEventsRepository(
private val _databaseProvider: IDatabaseProvider,
private val ioDispatcher: CoroutineDispatcher = OneSignalDispatchers.IO,
) : IOutcomeEventsRepository {
/**
* Delete event from the DB
*/
override suspend fun deleteOldOutcomeEvent(event: OutcomeEventParams) {
withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
_databaseProvider.os.delete(
OutcomeEventsTable.TABLE_NAME,
OutcomeEventsTable.COLUMN_NAME_TIMESTAMP + " = ?",
Expand All @@ -38,7 +36,7 @@ internal class OutcomeEventsRepository(
* For offline mode and contingency of errors
*/
override suspend fun saveOutcomeEvent(eventParams: OutcomeEventParams) {
withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
var notificationIds = JSONArray()
var iamIds = JSONArray()
var notificationInfluenceType = InfluenceType.UNATTRIBUTED
Expand Down Expand Up @@ -103,7 +101,7 @@ internal class OutcomeEventsRepository(
*/
override suspend fun getAllEventsToSend(): List<OutcomeEventParams> {
val events: MutableList<OutcomeEventParams> = ArrayList()
withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
RemoveInvalidSessionTimeRecords.run(_databaseProvider)
_databaseProvider.os.query(OutcomeEventsTable.TABLE_NAME) { cursor ->
if (cursor.moveToFirst()) {
Expand Down Expand Up @@ -250,7 +248,7 @@ internal class OutcomeEventsRepository(
override suspend fun saveUniqueOutcomeEventParams(eventParams: OutcomeEventParams) {
Logging.debug("OutcomeEventsCache.saveUniqueOutcomeEventParams(eventParams: $eventParams)")

withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
val outcomeName = eventParams.outcomeId
val cachedUniqueOutcomes: MutableList<CachedUniqueOutcome> = ArrayList()
val directBody = eventParams.outcomeSource?.directBody
Expand Down Expand Up @@ -285,7 +283,7 @@ internal class OutcomeEventsRepository(
): List<Influence> {
val uniqueInfluences: MutableList<Influence> = ArrayList()

withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
try {
for (influence in influences) {
val availableInfluenceIds = JSONArray()
Expand Down Expand Up @@ -335,7 +333,7 @@ internal class OutcomeEventsRepository(
val notificationTableName = OneSignalDbContract.NotificationTable.TABLE_NAME
val notificationIdColumnName = OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID

withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
val whereStr =
"NOT EXISTS(" +
"SELECT NULL FROM " + notificationTableName + " n " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import com.onesignal.common.services.ServiceBuilder
import com.onesignal.common.services.ServiceProvider
import com.onesignal.debug.LogLevel
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.mocks.TestDispatcherProvider
import com.onesignal.mocks.IOMockHelper
import com.onesignal.mocks.IOMockHelper.awaitIO
import io.kotest.assertions.throwables.shouldThrowUnit
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.comparables.shouldBeLessThan
Expand All @@ -13,12 +14,7 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest

@OptIn(ExperimentalCoroutinesApi::class)
class StartupServiceTests : FunSpec({
fun setupServiceProvider(
bootstrapServices: List<IBootstrapService>,
Expand All @@ -31,127 +27,111 @@ class StartupServiceTests : FunSpec({
serviceBuilder.register(reg).provides<IStartableService>()
return serviceBuilder.build()
}
val testDispatcher = StandardTestDispatcher()
val dispatcherProvider = TestDispatcherProvider(testDispatcher)

listener(IOMockHelper)

beforeAny {
Logging.logLevel = LogLevel.NONE
}

test("bootstrap with no IBootstrapService dependencies is a no-op") {
runTest(testDispatcher.scheduler) {
// Given
val startupService = StartupService(setupServiceProvider(listOf(), listOf()), dispatcherProvider)
// Given
val startupService = StartupService(setupServiceProvider(listOf(), listOf()))

// When
startupService.bootstrap()
// When
startupService.bootstrap()

// Then
}
// Then
}

test("bootstrap will call all IBootstrapService dependencies successfully") {
runTest(testDispatcher.scheduler) {
// Given
val mockBootstrapService1 = mockk<IBootstrapService>(relaxed = true)
val mockBootstrapService2 = mockk<IBootstrapService>(relaxed = true)
// Given
val mockBootstrapService1 = mockk<IBootstrapService>(relaxed = true)
val mockBootstrapService2 = mockk<IBootstrapService>(relaxed = true)

val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider)
val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()))

// When
startupService.bootstrap()
// When
startupService.bootstrap()

// Then
verify(exactly = 1) { mockBootstrapService1.bootstrap() }
verify(exactly = 1) { mockBootstrapService2.bootstrap() }
}
// Then
verify(exactly = 1) { mockBootstrapService1.bootstrap() }
verify(exactly = 1) { mockBootstrapService2.bootstrap() }
}

test("bootstrap will propagate exception when an IBootstrapService throws an exception") {
runTest(testDispatcher.scheduler) {
// Given
val exception = Exception("SOMETHING BAD")

val mockBootstrapService1 = mockk<IBootstrapService>()
every { mockBootstrapService1.bootstrap() } throws exception
val mockBootstrapService2 = spyk<IBootstrapService>()

val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider)

// When
val actualException =
shouldThrowUnit<Exception> {
startupService.bootstrap()
}

// Then
actualException shouldBe exception
verify(exactly = 1) { mockBootstrapService1.bootstrap() }
verify(exactly = 0) { mockBootstrapService2.bootstrap() }
}
// Given
val exception = Exception("SOMETHING BAD")

val mockBootstrapService1 = mockk<IBootstrapService>()
every { mockBootstrapService1.bootstrap() } throws exception
val mockBootstrapService2 = spyk<IBootstrapService>()

val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()))

// When
val actualException =
shouldThrowUnit<Exception> {
startupService.bootstrap()
}

// Then
actualException shouldBe exception
verify(exactly = 1) { mockBootstrapService1.bootstrap() }
verify(exactly = 0) { mockBootstrapService2.bootstrap() }
}

test("startup will call all IStartableService dependencies successfully after a short delay") {
runTest(testDispatcher.scheduler) {
// Given
val mockStartupService1 = mockk<IStartableService>(relaxed = true)
val mockStartupService2 = mockk<IStartableService>(relaxed = true)

val startupService = StartupService(
setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2)),
dispatcherProvider
)

// When
startupService.scheduleStart()

// Then - wait deterministically for both services to start using advanceUntilIdle
advanceUntilIdle()
verify(exactly = 1) { mockStartupService1.start() }
verify(exactly = 1) { mockStartupService2.start() }
}
// Given
val mockStartupService1 = mockk<IStartableService>(relaxed = true)
val mockStartupService2 = mockk<IStartableService>(relaxed = true)

val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2)))

// When
startupService.scheduleStart()

// Then - wait deterministically for both services to start using IOMockHelper
awaitIO()
verify(exactly = 1) { mockStartupService1.start() }
verify(exactly = 1) { mockStartupService2.start() }
}

test("scheduleStart does not block main thread") {
runTest(testDispatcher.scheduler) {
// Given
val mockStartableService1 = mockk<IStartableService>(relaxed = true)
val mockStartableService2 = spyk<IStartableService>()
val mockStartableService3 = spyk<IStartableService>()
// Only service1 and service2 are scheduled - service3 is NOT scheduled
val startupService = StartupService(
setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2)),
dispatcherProvider
)

// When - scheduleStart() is async, so it doesn't block
val startTime = System.currentTimeMillis()
startupService.scheduleStart()
val scheduleTime = System.currentTimeMillis() - startTime

// This should execute immediately since scheduleStart() doesn't block
// service3 is NOT part of scheduled services, so this is a direct call
mockStartableService3.start()
val immediateTime = System.currentTimeMillis() - startTime

// Then - verify scheduleStart() returned quickly (non-blocking)
// Should return in < 50ms (proving it doesn't wait for services to start)
scheduleTime shouldBeLessThan 50L
immediateTime shouldBeLessThan 50L

// Verify service3 was called immediately (proving main thread wasn't blocked)
verify(exactly = 1) { mockStartableService3.start() }

// Wait deterministically for async execution using advanceUntilIdle
advanceUntilIdle()

// Verify scheduled services were called
verify(exactly = 1) { mockStartableService1.start() }
verify(exactly = 1) { mockStartableService2.start() }

// The key assertion: scheduleStart() returned immediately without blocking,
// allowing service3.start() to be called synchronously before scheduled services
// complete. This proves scheduleStart() is non-blocking.
}
// Given
val mockStartableService1 = mockk<IStartableService>(relaxed = true)
val mockStartableService2 = spyk<IStartableService>()
val mockStartableService3 = spyk<IStartableService>()
// Only service1 and service2 are scheduled - service3 is NOT scheduled
val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2)))

// When - scheduleStart() is async, so it doesn't block
val startTime = System.currentTimeMillis()
startupService.scheduleStart()
val scheduleTime = System.currentTimeMillis() - startTime

// This should execute immediately since scheduleStart() doesn't block
// service3 is NOT part of scheduled services, so this is a direct call
mockStartableService3.start()
val immediateTime = System.currentTimeMillis() - startTime

// Then - verify scheduleStart() returned quickly (non-blocking)
// Should return in < 50ms (proving it doesn't wait for services to start)
scheduleTime shouldBeLessThan 50L
immediateTime shouldBeLessThan 50L

// Verify service3 was called immediately (proving main thread wasn't blocked)
verify(exactly = 1) { mockStartableService3.start() }

// Wait deterministically for async execution using IOMockHelper
awaitIO()

// Verify scheduled services were called
verify(exactly = 1) { mockStartableService1.start() }
verify(exactly = 1) { mockStartableService2.start() }

// The key assertion: scheduleStart() returned immediately without blocking,
// allowing service3.start() to be called synchronously before scheduled services
// complete. This proves scheduleStart() is non-blocking.
}
})
Loading
Loading