From 994629e79e26dffaf2b09758251824f146501db3 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 17 Mar 2025 10:39:41 -0400 Subject: [PATCH 1/2] Simplify settings package and a few more classes using DI --- .../sessions/FirebaseSessionsComponent.kt | 81 +++++++++++++++- .../firebase/sessions/SessionDatastore.kt | 20 +--- .../firebase/sessions/SessionGenerator.kt | 11 +-- .../google/firebase/sessions/TimeProvider.kt | 5 +- .../google/firebase/sessions/UuidGenerator.kt | 29 ++++++ .../settings/LocalOverrideSettings.kt | 15 ++- .../sessions/settings/RemoteSettings.kt | 19 ++-- .../settings/RemoteSettingsFetcher.kt | 17 ++-- .../sessions/settings/SessionsSettings.kt | 73 ++------------ .../sessions/settings/SettingsCache.kt | 15 +-- .../firebase/sessions/SessionGeneratorTest.kt | 41 ++------ .../sessions/SessionLifecycleClientTest.kt | 25 ++--- .../sessions/SessionLifecycleServiceTest.kt | 40 +++----- .../SessionsActivityLifecycleCallbacksTest.kt | 25 ++--- .../sessions/settings/RemoteSettingsTest.kt | 97 +++++++++---------- .../sessions/settings/SessionsSettingsTest.kt | 43 +++++--- .../firebase/sessions/testing/FakeLazy.kt | 26 +++++ .../sessions/testing/FakeUuidGenerator.kt | 37 +++++++ .../testing/FirebaseSessionsFakeComponent.kt | 44 ++++++--- .../testing/FirebaseSessionsFakeRegistrar.kt | 75 ++------------ 20 files changed, 392 insertions(+), 346 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index aa60f3f41df..5680c9cc0ec 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -17,22 +17,48 @@ package com.google.firebase.sessions import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.inject.Provider import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName +import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher +import com.google.firebase.sessions.settings.LocalOverrideSettings +import com.google.firebase.sessions.settings.RemoteSettings +import com.google.firebase.sessions.settings.RemoteSettingsFetcher import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider import dagger.Binds import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Qualifier import javax.inject.Singleton import kotlin.coroutines.CoroutineContext -/** Dagger component to provide [FirebaseSessions] and its dependencies. */ +@Qualifier internal annotation class SessionConfigsDataStore + +@Qualifier internal annotation class SessionDetailsDataStore + +@Qualifier internal annotation class LocalOverrideSettingsProvider + +@Qualifier internal annotation class RemoteSettingsProvider + +/** + * Dagger component to provide [FirebaseSessions] and its dependencies. + * + * This gets configured and built in [FirebaseSessionsRegistrar.getComponents]. + */ @Singleton @Component(modules = [FirebaseSessionsComponent.MainModule::class]) internal interface FirebaseSessionsComponent { @@ -79,8 +105,59 @@ internal interface FirebaseSessionsComponent { impl: SessionLifecycleServiceBinderImpl ): SessionLifecycleServiceBinder + @Binds + @Singleton + fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher + + @Binds + @Singleton + @LocalOverrideSettingsProvider + fun localOverrideSettings(impl: LocalOverrideSettings): SettingsProvider + + @Binds + @Singleton + @RemoteSettingsProvider + fun remoteSettings(impl: RemoteSettings): SettingsProvider + companion object { - @Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock) + private const val TAG = "FirebaseSessions" + + @Provides @Singleton fun timeProvider(): TimeProvider = TimeProviderImpl + + @Provides @Singleton fun uuidGenerator(): UuidGenerator = UuidGeneratorImpl + + @Provides + @Singleton + fun applicationInfo(firebaseApp: FirebaseApp): ApplicationInfo = + SessionEvents.getApplicationInfo(firebaseApp) + + @Provides + @Singleton + @SessionConfigsDataStore + fun sessionConfigsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME) + } + + @Provides + @Singleton + @SessionDetailsDataStore + fun sessionDetailsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME) + } } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index a2d46a48891..2c4f243f942 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -16,19 +16,15 @@ package com.google.firebase.sessions -import android.content.Context import android.util.Log import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import java.io.IOException import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -64,8 +60,8 @@ internal interface SessionDatastore { internal class SessionDatastoreImpl @Inject constructor( - private val appContext: Context, @Background private val backgroundDispatcher: CoroutineContext, + @SessionDetailsDataStore private val dataStore: DataStore, ) : SessionDatastore { /** Most recent session from datastore is updated asynchronously whenever it changes */ @@ -76,7 +72,7 @@ constructor( } private val firebaseSessionDataFlow: Flow = - appContext.dataStore.data + dataStore.data .catch { exception -> Log.e(TAG, "Error reading stored session data.", exception) emit(emptyPreferences()) @@ -92,7 +88,7 @@ constructor( override fun updateSessionId(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { try { - appContext.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId } } catch (e: IOException) { @@ -108,15 +104,5 @@ constructor( private companion object { private const val TAG = "FirebaseSessionsRepo" - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SESSIONS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 41aeb442cfb..4c4775e8b24 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -19,7 +19,7 @@ package com.google.firebase.sessions import com.google.errorprone.annotations.CanIgnoreReturnValue import com.google.firebase.Firebase import com.google.firebase.app -import java.util.UUID +import javax.inject.Inject import javax.inject.Singleton /** @@ -37,10 +37,9 @@ internal data class SessionDetails( * [SessionDetails] up to date with the latest values. */ @Singleton -internal class SessionGenerator( - private val timeProvider: TimeProvider, - private val uuidGenerator: () -> UUID = UUID::randomUUID, -) { +internal class SessionGenerator +@Inject +constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) { private val firstSessionId = generateSessionId() private var sessionIndex = -1 @@ -66,7 +65,7 @@ internal class SessionGenerator( return currentSession } - private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase() + private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase() internal companion object { val instance: SessionGenerator diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index 706285de337..b66b09af19f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -23,11 +23,12 @@ import kotlin.time.Duration.Companion.milliseconds /** Time provider interface, for testing purposes. */ internal interface TimeProvider { fun elapsedRealtime(): Duration + fun currentTimeUs(): Long } -/** "Wall clock" time provider. */ -internal object WallClock : TimeProvider { +/** "Wall clock" time provider implementation. */ +internal object TimeProviderImpl : TimeProvider { /** * Gets the [Duration] elapsed in "wall clock" time since device boot. * diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt new file mode 100644 index 00000000000..8c5b153fef2 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions + +import java.util.UUID + +/** UUID generator interface. */ +internal fun interface UuidGenerator { + fun next(): UUID +} + +/** Generate random UUIDs using [UUID.randomUUID]. */ +internal object UuidGeneratorImpl : UuidGenerator { + override fun next(): UUID = UUID.randomUUID() +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt index 37e7acc949b..f13d0ffde2e 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt @@ -19,20 +19,19 @@ package com.google.firebase.sessions.settings import android.content.Context import android.content.pm.PackageManager import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration -internal class LocalOverrideSettings(context: Context) : SettingsProvider { - @Suppress("DEPRECATION") // TODO(mrober): Use ApplicationInfoFlags when target sdk set to 33 +@Singleton +internal class LocalOverrideSettings @Inject constructor(appContext: Context) : SettingsProvider { private val metadata = - context.packageManager - .getApplicationInfo( - context.packageName, - PackageManager.GET_META_DATA, - ) + appContext.packageManager + .getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA) .metaData - ?: Bundle.EMPTY // Default to an empty bundle, meaning no cached values. + ?: Bundle.EMPTY // Default to an empty bundle override val sessionEnabled: Boolean? get() = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt index 1e6015a5c0d..67a48bc7924 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt @@ -19,11 +19,13 @@ package com.google.firebase.sessions.settings import android.os.Build import android.util.Log import androidx.annotation.VisibleForTesting -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.InstallationId +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -34,14 +36,19 @@ import kotlinx.coroutines.sync.withLock import org.json.JSONException import org.json.JSONObject -internal class RemoteSettings( - private val backgroundDispatcher: CoroutineContext, +@Singleton +internal class RemoteSettings +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineContext, private val firebaseInstallationsApi: FirebaseInstallationsApi, private val appInfo: ApplicationInfo, private val configsFetcher: CrashlyticsSettingsFetcher, - dataStore: DataStore, + private val lazySettingsCache: Lazy, ) : SettingsProvider { - private val settingsCache by lazy { SettingsCache(dataStore) } + private val settingsCache: SettingsCache + get() = lazySettingsCache.get() + private val fetchInProgress = Mutex() override val sessionEnabled: Boolean? diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt index a0896c24e7e..92d530f2fa1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt @@ -17,10 +17,13 @@ package com.google.firebase.sessions.settings import android.net.Uri +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.sessions.ApplicationInfo import java.io.BufferedReader import java.io.InputStreamReader import java.net.URL +import javax.inject.Inject +import javax.inject.Singleton import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @@ -30,20 +33,22 @@ internal fun interface CrashlyticsSettingsFetcher { suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (msg: String) -> Unit + onFailure: suspend (msg: String) -> Unit, ) } -internal class RemoteSettingsFetcher( +@Singleton +internal class RemoteSettingsFetcher +@Inject +constructor( private val appInfo: ApplicationInfo, - private val blockingDispatcher: CoroutineContext, - private val baseUrl: String = FIREBASE_SESSIONS_BASE_URL_STRING, + @Background private val blockingDispatcher: CoroutineContext, ) : CrashlyticsSettingsFetcher { @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls. override suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (String) -> Unit + onFailure: suspend (String) -> Unit, ) = withContext(blockingDispatcher) { try { @@ -78,7 +83,7 @@ internal class RemoteSettingsFetcher( val uri = Uri.Builder() .scheme("https") - .authority(baseUrl) + .authority(FIREBASE_SESSIONS_BASE_URL_STRING) .appendPath("spi") .appendPath("v2") .appendPath("platforms") diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index 41b73f14a4e..d319bebb7a2 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -16,71 +16,24 @@ package com.google.firebase.sessions.settings -import android.content.Context -import android.util.Log -import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.app -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.FirebaseSessionsComponent -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName -import com.google.firebase.sessions.SessionDataStoreConfigs -import com.google.firebase.sessions.SessionEvents +import com.google.firebase.sessions.LocalOverrideSettingsProvider +import com.google.firebase.sessions.RemoteSettingsProvider import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes /** [SessionsSettings] manages all the configs that are relevant to the sessions library. */ @Singleton -internal class SessionsSettings( - private val localOverrideSettings: SettingsProvider, - private val remoteSettings: SettingsProvider, +internal class SessionsSettings +@Inject +constructor( + @LocalOverrideSettingsProvider private val localOverrideSettings: SettingsProvider, + @RemoteSettingsProvider private val remoteSettings: SettingsProvider, ) { - private constructor( - context: Context, - blockingDispatcher: CoroutineContext, - backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - appInfo: ApplicationInfo, - ) : this( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = - RemoteSettings( - backgroundDispatcher, - firebaseInstallationsApi, - appInfo, - configsFetcher = - RemoteSettingsFetcher( - appInfo, - blockingDispatcher, - ), - dataStore = context.dataStore, - ), - ) - - @Inject - constructor( - firebaseApp: FirebaseApp, - @Blocking blockingDispatcher: CoroutineContext, - @Background backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - ) : this( - firebaseApp.applicationContext, - blockingDispatcher, - backgroundDispatcher, - firebaseInstallationsApi, - SessionEvents.getApplicationInfo(firebaseApp), - ) // Order of preference for all the configs below: // 1. Honor local overrides @@ -147,19 +100,7 @@ internal class SessionsSettings( } internal companion object { - private const val TAG = "SessionsSettings" - val instance: SessionsSettings get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionsSettings - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SETTINGS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt index 33b6a4fe7c8..2e60e51650a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt @@ -25,7 +25,10 @@ import androidx.datastore.preferences.core.doublePreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey +import com.google.firebase.sessions.SessionConfigsDataStore import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -37,7 +40,10 @@ internal data class SessionConfigs( val cacheUpdatedTime: Long?, ) -internal class SettingsCache(private val dataStore: DataStore) { +@Singleton +internal class SettingsCache +@Inject +constructor(@SessionConfigsDataStore private val dataStore: DataStore) { private lateinit var sessionConfigs: SessionConfigs init { @@ -54,7 +60,7 @@ internal class SettingsCache(private val dataStore: DataStore) { sessionSamplingRate = preferences[SAMPLING_RATE], sessionRestartTimeout = preferences[RESTART_TIMEOUT_SECONDS], cacheDuration = preferences[CACHE_DURATION_SECONDS], - cacheUpdatedTime = preferences[CACHE_UPDATED_TIME] + cacheUpdatedTime = preferences[CACHE_UPDATED_TIME], ) } @@ -105,10 +111,7 @@ internal class SettingsCache(private val dataStore: DataStore) { updateSessionConfigs(preferences) } } catch (e: IOException) { - Log.w( - TAG, - "Failed to remove config values: $e", - ) + Log.w(TAG, "Failed to remove config values: $e") } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index 7f29fb66ae7..7126bae4dbf 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -18,8 +18,8 @@ package com.google.firebase.sessions import com.google.common.truth.Truth.assertThat import com.google.firebase.sessions.testing.FakeTimeProvider +import com.google.firebase.sessions.testing.FakeUuidGenerator import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US -import java.util.UUID import org.junit.Test class SessionGeneratorTest { @@ -41,9 +41,7 @@ class SessionGeneratorTest { @Test(expected = UninitializedPropertyAccessException::class) fun currentSession_beforeGenerate_throwsUninitialized() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.currentSession } @@ -51,9 +49,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_beforeGenerate_returnsFalse() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) assertThat(sessionGenerator.hasGenerateSession).isFalse() } @@ -61,9 +57,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_afterGenerate_returnsTrue() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -73,9 +67,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionIds() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -91,10 +83,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionDetails() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) sessionGenerator.generateNewSession() @@ -117,10 +106,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_incrementsSessionIndex_keepsFirstSessionId() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) val firstSessionDetails = sessionGenerator.generateNewSession() @@ -170,22 +156,9 @@ class SessionGeneratorTest { ) } - private class UUIDs(val names: List = listOf(UUID_1, UUID_2, UUID_3)) { - var index = -1 - - fun next(): UUID { - index = (index + 1).coerceAtMost(names.size - 1) - return UUID.fromString(names[index]) - } - } - - @Suppress("SpellCheckingInspection") // UUIDs are not words. companion object { - const val UUID_1 = "11111111-1111-1111-1111-111111111111" const val SESSION_ID_1 = "11111111111111111111111111111111" - const val UUID_2 = "22222222-2222-2222-2222-222222222222" const val SESSION_ID_2 = "22222222222222222222222222222222" - const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" const val SESSION_ID_3 = "cccccccccccccccccccccccccccccccc" } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt index b038e68081c..12a017a7462 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -47,21 +48,21 @@ import org.robolectric.Shadows.shadowOf @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleClientTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder @Before fun setUp() { - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt index 682a9ddfbbb..ccd933f1213 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt @@ -16,7 +16,6 @@ package com.google.firebase.sessions -import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper @@ -30,10 +29,8 @@ import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.initialize import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FakeFirelogPublisher -import com.google.firebase.sessions.testing.FakeSessionDatastore +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import java.time.Duration -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Before import org.junit.Test @@ -46,14 +43,11 @@ import org.robolectric.annotation.LooperMode import org.robolectric.annotation.LooperMode.Mode.PAUSED import org.robolectric.shadows.ShadowSystemClock -@OptIn(ExperimentalCoroutinesApi::class) @MediumTest @LooperMode(PAUSED) @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleServiceTest { - - lateinit var service: ServiceController - lateinit var firebaseApp: FirebaseApp + private lateinit var service: ServiceController data class CallbackMessage(val code: Int, val sessionId: String?) @@ -68,16 +62,14 @@ internal class SessionLifecycleServiceTest { @Before fun setUp() { - val context = ApplicationProvider.getApplicationContext() - firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build() - ) + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) service = createService() } @@ -99,7 +91,7 @@ internal class SessionLifecycleServiceTest { @Test fun binding_callbackOnInitialBindWhenSessionIdSet() { val client = TestCallbackHandler() - firebaseApp.get(FakeSessionDatastore::class.java).updateSessionId("123") + FirebaseSessionsFakeComponent.instance.fakeSessionDatastore.updateSessionId("123") bindToService(client) @@ -222,11 +214,9 @@ internal class SessionLifecycleServiceTest { } private fun createServiceLaunchIntent(client: TestCallbackHandler) = - Intent( - ApplicationProvider.getApplicationContext(), - SessionLifecycleService::class.java - ) - .apply { putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) } + Intent(ApplicationProvider.getApplicationContext(), SessionLifecycleService::class.java).apply { + putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) + } private fun createService() = Robolectric.buildService(SessionLifecycleService::class.java).create() @@ -237,7 +227,7 @@ internal class SessionLifecycleServiceTest { } private fun getUploadedSessions() = - firebaseApp.get(FakeFirelogPublisher::class.java).loggedSessions + FirebaseSessionsFakeComponent.instance.fakeFirelogPublisher.loggedSessions private fun getSessionId(msg: Message) = msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt index 189e13fed89..62e650d90c8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher @@ -46,7 +47,7 @@ import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) internal class SessionsActivityLifecycleCallbacksTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder private val fakeActivity = Activity() @Before @@ -63,17 +64,17 @@ internal class SessionsActivityLifecycleCallbacksTest { ) ) - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index 6a3a4a1f8c3..806861e2173 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -25,8 +25,8 @@ import com.google.firebase.concurrent.TestOnlyExecutors import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations +import com.google.firebase.sessions.testing.FakeLazy import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher -import com.google.firebase.sessions.testing.TestSessionEventData.TEST_APPLICATION_INFO import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers @@ -55,19 +55,21 @@ class RemoteSettingsTest { val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcher = FakeRemoteConfigFetcher() - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext - val remoteSettings = RemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) runCurrent() @@ -102,11 +104,15 @@ class RemoteSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) runCurrent() @@ -143,11 +149,15 @@ class RemoteSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -195,11 +205,15 @@ class RemoteSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -248,10 +262,7 @@ class RemoteSettingsTest { val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcherWithDelay = - FakeRemoteConfigFetcher( - JSONObject(VALID_RESPONSE), - networkDelay = 3.seconds, - ) + FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) fakeFetcherWithDelay.responseJSONObject .getJSONObject("app_quality") @@ -263,11 +274,15 @@ class RemoteSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), configsFetcher = fakeFetcherWithDelay, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) // Do the first fetch. This one should fetched the configsFetcher. @@ -290,24 +305,6 @@ class RemoteSettingsTest { assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125) } - @Test - fun remoteSettingsFetcher_badFetch_callsOnFailure() = runTest { - var failure: String? = null - - RemoteSettingsFetcher( - TEST_APPLICATION_INFO, - TestOnlyExecutors.blocking().asCoroutineDispatcher() + coroutineContext, - baseUrl = "this.url.is.invalid", - ) - .doConfigFetch( - headerOptions = emptyMap(), - onSuccess = {}, - onFailure = { failure = it }, - ) - - assertThat(failure).isNotNull() - } - @After fun cleanUp() { FirebaseApp.clearInstancesForTest() diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index f74eac409e5..5cb4b9973f5 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -26,6 +26,7 @@ import com.google.firebase.sessions.SessionDataStoreConfigs import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations +import com.google.firebase.sessions.testing.FakeLazy import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher import com.google.firebase.sessions.testing.FakeSettingsProvider import kotlin.time.Duration.Companion.minutes @@ -112,11 +113,15 @@ class SessionsSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) val sessionsSettings = @@ -154,11 +159,15 @@ class SessionsSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) val sessionsSettings = @@ -202,11 +211,15 @@ class SessionsSettingsTest { firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + lazySettingsCache = + FakeLazy { + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ) + }, ) val sessionsSettings = diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt new file mode 100644 index 00000000000..1b603a02b67 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions.testing + +import dagger.Lazy + +/** Fake implementation of a Dagger lazy singleton provider. */ +class FakeLazy(initializer: () -> T) : Lazy { + private val instance: T by lazy(initializer) + + override fun get(): T = instance +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt new file mode 100644 index 00000000000..88f1f816c12 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions.testing + +import com.google.firebase.sessions.UuidGenerator +import java.util.UUID + +/** Fake implementation of [UuidGenerator] to provide uuids of the given names in order. */ +internal class FakeUuidGenerator(private val names: List = listOf(UUID_1, UUID_2, UUID_3)) : + UuidGenerator { + private var index = -1 + + override fun next(): UUID { + index = (index + 1).coerceAtMost(names.size - 1) + return UUID.fromString(names[index]) + } + + companion object { + const val UUID_1 = "11111111-1111-1111-1111-111111111111" + const val UUID_2 = "22222222-2222-2222-2222-222222222222" + const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt index eda16d8f0b4..b3431f71840 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt @@ -23,24 +23,46 @@ import com.google.firebase.sessions.FirebaseSessionsComponent import com.google.firebase.sessions.SessionDatastore import com.google.firebase.sessions.SessionFirelogPublisher import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.SessionLifecycleServiceBinder import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider -/** Bridge between FirebaseSessionsComponent and FirebaseSessionsFakeRegistrar. */ +/** Fake component to manage [FirebaseSessions] and related, often faked, dependencies. */ +@Suppress("MemberVisibilityCanBePrivate") // Keep access to fakes open for convenience internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent { - // TODO(mrober): Move tests to use Dagger for DI. + // TODO(mrober): Move tests that need DI to integration tests, and remove this component. + + // Fakes, access these instances to setup test cases, e.g., add interval to fake time provider. + val fakeTimeProvider = FakeTimeProvider() + val fakeUuidGenerator = FakeUuidGenerator() + val fakeSessionDatastore = FakeSessionDatastore() + val fakeFirelogPublisher = FakeFirelogPublisher() + val fakeSessionLifecycleServiceBinder = FakeSessionLifecycleServiceBinder() + + // Settings providers, default to fake, set these to real instances for relevant test cases. + var localOverrideSettings: SettingsProvider = FakeSettingsProvider() + var remoteSettings: SettingsProvider = FakeSettingsProvider() override val firebaseSessions: FirebaseSessions - get() = Firebase.app[FirebaseSessions::class.java] + get() = throw NotImplementedError("FirebaseSessions not implemented, use integration tests.") + + override val sessionDatastore: SessionDatastore = fakeSessionDatastore + + override val sessionFirelogPublisher: SessionFirelogPublisher = fakeFirelogPublisher - override val sessionDatastore: SessionDatastore - get() = Firebase.app[SessionDatastore::class.java] + override val sessionGenerator: SessionGenerator by lazy { + SessionGenerator(timeProvider = fakeTimeProvider, uuidGenerator = fakeUuidGenerator) + } - override val sessionFirelogPublisher: SessionFirelogPublisher - get() = Firebase.app[SessionFirelogPublisher::class.java] + override val sessionsSettings: SessionsSettings by lazy { + SessionsSettings(localOverrideSettings, remoteSettings) + } - override val sessionGenerator: SessionGenerator - get() = Firebase.app[SessionGenerator::class.java] + val sessionLifecycleServiceBinder: SessionLifecycleServiceBinder + get() = fakeSessionLifecycleServiceBinder - override val sessionsSettings: SessionsSettings - get() = Firebase.app[SessionsSettings::class.java] + companion object { + val instance: FirebaseSessionsFakeComponent + get() = Firebase.app[FirebaseSessionsFakeComponent::class.java] + } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index 58855f622f3..8dc6454931e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -16,85 +16,30 @@ package com.google.firebase.sessions.testing -import androidx.annotation.Keep -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency -import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.platforminfo.LibraryVersionComponent import com.google.firebase.sessions.BuildConfig import com.google.firebase.sessions.FirebaseSessions import com.google.firebase.sessions.FirebaseSessionsComponent -import com.google.firebase.sessions.SessionDatastore -import com.google.firebase.sessions.SessionFirelogPublisher -import com.google.firebase.sessions.SessionGenerator -import com.google.firebase.sessions.SessionLifecycleServiceBinder -import com.google.firebase.sessions.WallClock -import com.google.firebase.sessions.settings.SessionsSettings -import kotlinx.coroutines.CoroutineDispatcher /** * [ComponentRegistrar] for setting up Fake components for [FirebaseSessions] and its internal * dependencies for unit tests. - * - * @hide */ -@Keep internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { override fun getComponents() = listOf( - Component.builder(SessionGenerator::class.java) - .name("session-generator") - .factory { SessionGenerator(timeProvider = WallClock) } - .build(), - Component.builder(FakeFirelogPublisher::class.java) - .name("fake-session-publisher") - .factory { FakeFirelogPublisher() } - .build(), - Component.builder(SessionFirelogPublisher::class.java) - .name("session-publisher") - .add(Dependency.required(fakeFirelogPublisher)) - .factory { container -> container.get(fakeFirelogPublisher) } - .build(), - Component.builder(SessionsSettings::class.java) - .name("sessions-settings") - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - SessionsSettings( - container.get(firebaseApp), - container.get(blockingDispatcher), - container.get(backgroundDispatcher), - fakeFirebaseInstallations, - ) - } - .build(), Component.builder(FirebaseSessionsComponent::class.java) - .name("fake-fire-sessions-component") - .factory { FirebaseSessionsFakeComponent() } - .build(), - Component.builder(FakeSessionDatastore::class.java) - .name("fake-sessions-datastore") - .factory { FakeSessionDatastore() } - .build(), - Component.builder(SessionDatastore::class.java) - .name("sessions-datastore") - .add(Dependency.required(fakeDatastore)) - .factory { container -> container.get(fakeDatastore) } + .name("fire-sessions-component") + .add(Dependency.required(firebaseSessionsFakeComponent)) + .factory { container -> container.get(firebaseSessionsFakeComponent) } .build(), - Component.builder(FakeSessionLifecycleServiceBinder::class.java) - .name("fake-sessions-service-binder") - .factory { FakeSessionLifecycleServiceBinder() } - .build(), - Component.builder(SessionLifecycleServiceBinder::class.java) - .name("sessions-service-binder") - .add(Dependency.required(fakeServiceBinder)) - .factory { container -> container.get(fakeServiceBinder) } + Component.builder(FirebaseSessionsFakeComponent::class.java) + .name("fire-sessions-fake-component") + .factory { FirebaseSessionsFakeComponent() } .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) @@ -102,12 +47,6 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { private companion object { const val LIBRARY_NAME = "fire-sessions" - val firebaseApp = unqualified(FirebaseApp::class.java) - val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) - val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) - val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) - val fakeDatastore = unqualified(FakeSessionDatastore::class.java) - val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) - val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val firebaseSessionsFakeComponent = unqualified(FirebaseSessionsFakeComponent::class.java) } } From 2b08850dfc771847441a9a479c9e01929cf007e2 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 18 Mar 2025 09:18:47 -0400 Subject: [PATCH 2/2] Fix unit tests --- .../sessions/settings/RemoteSettingsTest.kt | 118 ++++++++++-------- .../sessions/settings/SessionsSettingsTest.kt | 52 ++++---- .../firebase/sessions/testing/FakeLazy.kt | 26 ---- 3 files changed, 86 insertions(+), 110 deletions(-) delete mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index 806861e2173..e4fb0b00148 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -22,11 +22,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations -import com.google.firebase.sessions.testing.FakeLazy import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher +import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers @@ -56,20 +58,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) runCurrent() @@ -99,20 +98,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) runCurrent() @@ -144,20 +140,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -200,20 +193,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -269,20 +259,17 @@ class RemoteSettingsTest { .put("sampling_rate", 0.125) val remoteSettingsWithDelay = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), - configsFetcher = fakeFetcherWithDelay, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + fakeFetcherWithDelay, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) // Do the first fetch. This one should fetched the configsFetcher. @@ -310,7 +297,7 @@ class RemoteSettingsTest { FirebaseApp.clearInstancesForTest() } - private companion object { + internal companion object { const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test" const val VALID_RESPONSE = @@ -331,5 +318,30 @@ class RemoteSettingsTest { } } """ + + /** + * Build an instance of [RemoteSettings] using the Dagger factory. + * + * This is needed because the SDK vendors Dagger to a difference namespace, but it does not for + * these unit tests. The [RemoteSettings.lazySettingsCache] has type [dagger.Lazy] in these + * tests, but type `com.google.firebase.sessions.dagger.Lazy` in the SDK. This method to build + * the instance is the easiest I could find that does not need any reference to [dagger.Lazy] in + * the test code. + */ + fun buildRemoteSettings( + backgroundDispatcher: CoroutineContext, + firebaseInstallationsApi: FirebaseInstallationsApi, + appInfo: ApplicationInfo, + configsFetcher: CrashlyticsSettingsFetcher, + settingsCache: SettingsCache, + ): RemoteSettings = + RemoteSettings_Factory.create( + { backgroundDispatcher }, + { firebaseInstallationsApi }, + { appInfo }, + { configsFetcher }, + { settingsCache }, + ) + .get() } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index 5cb4b9973f5..12f40e7cca8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -26,7 +26,6 @@ import com.google.firebase.sessions.SessionDataStoreConfigs import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations -import com.google.firebase.sessions.testing.FakeLazy import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher import com.google.firebase.sessions.testing.FakeSettingsProvider import kotlin.time.Duration.Companion.minutes @@ -108,20 +107,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) val sessionsSettings = @@ -154,20 +150,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) val sessionsSettings = @@ -206,20 +199,17 @@ class SessionsSettingsTest { fakeFetcher.responseJSONObject = JSONObject(invalidResponse) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - lazySettingsCache = - FakeLazy { - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ) - }, + SettingsCache( + PreferenceDataStoreFactory.create( + scope = this, + produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, + ) + ), ) val sessionsSettings = diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt deleted file mode 100644 index 1b603a02b67..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeLazy.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.sessions.testing - -import dagger.Lazy - -/** Fake implementation of a Dagger lazy singleton provider. */ -class FakeLazy(initializer: () -> T) : Lazy { - private val instance: T by lazy(initializer) - - override fun get(): T = instance -}