From 2895e2967c90e4ce8b441e18e206ec1726b91462 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 12 Jan 2022 15:09:56 -0800 Subject: [PATCH 1/2] prepare release 1.4.2 (#69) * prepare snapshot 1.4.3 * update RELEASING.md * try to resolve flaky test --- RELEASING.md | 29 +++++++++++-------- .../android/AndroidLifecyclePluginTests.kt | 2 +- .../analytics/kotlin/core/Constants.kt | 2 +- gradle.properties | 4 +-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index c2f67f1e..a1934743 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -9,20 +9,25 @@ Releasing 1. Create a new branch called `release/X.Y.Z` 2. `git checkout -b release/X.Y.Z` -3. Change the version to your desired release version (see `Update Version`) -4. `git commit -am "Prepare release X.Y.Z."` (where X.Y.Z is the new version) +3. Change the version in `gradle.properties` to your desired release version (see `Update Version`) +4. `git commit -am "Create release X.Y.Z."` (where X.Y.Z is the new version) 5. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) -6. `git push && git push --tags` -7. The CI pipeline will recognize the tag and upload, close and promote the artifacts, and generate changelog automatically -8. Create a PR to merge the new branch into `main` -9. The CI pipeline will trigger a snapshot workflow and upload the artifact. +6. Upgrade to next version by changing version in `gradle.properties` +7. `git commit -am "Prepare snapshot X.Y.Z-SNAPSHOT"` +8. `git push && git push --tags` +9. Create a PR to merge the new branch into `master` +10. The CI pipeline will recognize the tag and upload, close and promote the artifacts automatically, and generate changelog automatically Example (stable release) ======== -1. Current version is 1.3.0 +1. Current VERSION_NAME in `gradle.properties` = 1.3.0 2. `git checkout -b release/1.3.1` -3. Change version to 1.3.1 (next higher version, see `Update Version`) -4. `git commit -am "Prepare release 1.3.1"` -5. `git tag -a 1.3.1 -m "Version 1.3.1"` -6. `git push && git push --tags`. This tag push will create stable release 1.3.1 with auto-generated changelog -8. Create a PR to merge the new branch into `main`. Merging PR main will create a snapshot release 1.3.1-SNAPSHOT \ No newline at end of file +3. Change VERSION_NAME = 1.3.1 (next higher version) +4. Update CHANGELOG.md +5. `git commit -am "Create release 1.3.1` +6. `git tag -a 1.3.1 -m "Version 1.3.1"` +6. `git push && git push --tags` +7. Change VERSION_NAME = 1.3.2 (next higher version) +8. `git commit -am "Prepare snapshot 1.3.2-SNAPSHOT"` +9. `git push && git push --tags` +10. Merging PR master will create a snapshot release 1.3.2-SNAPSHOT and tag push will create stable release 1.3.1 \ No newline at end of file diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt index bd4b76ab..06409d04 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt @@ -193,7 +193,7 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - verify (timeout = 2000){ mockPlugin.updateState(true) } + verify (timeout = 4000){ mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/Constants.kt b/core/src/main/java/com/segment/analytics/kotlin/core/Constants.kt index 822d1ace..6c7a1e60 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/Constants.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/Constants.kt @@ -1,7 +1,7 @@ package com.segment.analytics.kotlin.core object Constants { - const val LIBRARY_VERSION = "1.4.2" + const val LIBRARY_VERSION = "1.4.3" const val DEFAULT_API_HOST = "api.segment.io/v1" const val DEFAULT_CDN_HOST = "cdn-settings.segment.com/v1" } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 65ae2683..9524f32f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,8 +23,8 @@ kotlin.code.style=official # Deployment variables GROUP=com.segment.analytics.kotlin -VERSION_CODE=142 -VERSION_NAME=1.4.2 +VERSION_CODE=143 +VERSION_NAME=1.4.3 POM_NAME=Segment for Kotlin POM_DESCRIPTION=The hassle-free way to add analytics to your Kotlin app. From 247f49d540805696a7746f487e10212564934606 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 16 Feb 2022 16:12:42 -0600 Subject: [PATCH 2/2] fix ANR caused by DRM api (#71) * make DRM API async * make analytics inheritable and delegate coroutine config out * add test analytics that fully runs synchronizely * replace with the new testAnalytics * remove verify timeout * update coroutine version to 1.6.0 * replace everything with the new test api * replace runBlocking with runTest * bug fix * bug fix * remove unused import * use empty string as default device id * cache device id --- android/build.gradle | 6 +- .../android/plugins/AndroidContextPlugin.kt | 48 ++++++++++++++-- .../android/AndroidContextCollectorTests.kt | 55 +++++++++++++++---- .../android/AndroidLifecyclePluginTests.kt | 32 ++++++----- .../kotlin/android/EventsFileTests.kt | 19 +++---- .../analytics/kotlin/android/StorageTests.kt | 26 ++++----- .../analytics/kotlin/android/utils/Mocks.kt | 47 +++++++++++----- core/build.gradle | 4 +- .../analytics/kotlin/core/Analytics.kt | 48 +++++++++------- .../analytics/kotlin/core/Configuration.kt | 14 ++++- .../segment/analytics/kotlin/core/Storage.kt | 3 +- .../analytics/kotlin/core/AnalyticsTests.kt | 31 +++++------ .../analytics/kotlin/core/PluginTests.kt | 6 +- .../analytics/kotlin/core/SettingsTests.kt | 34 +++++------- .../analytics/kotlin/core/StateTest.kt | 25 ++++----- .../kotlin/core/compat/JavaAnalyticsTest.kt | 32 +++++------ .../core/platform/DestinationPluginTests.kt | 7 ++- .../kotlin/core/platform/EventPipelineTest.kt | 26 +++++---- .../plugins/DeviceTokenPluginTests.kt | 15 ++--- .../core/platform/plugins/LogTargetTest.kt | 15 ++--- .../plugins/SegmentDestinationTests.kt | 38 ++++++------- .../core/platform/plugins/SegmentLogTest.kt | 17 +++--- .../core/platform/plugins/StartupQueueTest.kt | 14 ++--- .../core/utilities/EventsFileManagerTest.kt | 16 +++--- .../kotlin/core/utilities/StorageImplTest.kt | 31 ++++++----- .../analytics/kotlin/core/utils/Mocks.kt | 46 +++++++++++----- .../build.gradle | 2 +- .../plugins/ComscoreDestinationTests.kt | 8 +-- .../plugins/IntercomDestinationTest.kt | 4 +- samples/kotlin-android-app/build.gradle | 4 +- samples/kotlin-jvm-app/build.gradle | 2 +- 31 files changed, 399 insertions(+), 276 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 0b7acce9..389df7df 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,8 +54,8 @@ dependencies { api project(':core') api 'com.segment:sovran-kotlin:1.2.1' api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' implementation 'androidx.lifecycle:lifecycle-process:2.4.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0' @@ -64,7 +64,7 @@ dependencies { // TESTING testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' testImplementation 'io.mockk:mockk:1.10.6' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/android/src/main/java/com/segment/analytics/kotlin/android/plugins/AndroidContextPlugin.kt b/android/src/main/java/com/segment/analytics/kotlin/android/plugins/AndroidContextPlugin.kt index 42947657..d5c35d51 100644 --- a/android/src/main/java/com/segment/analytics/kotlin/android/plugins/AndroidContextPlugin.kt +++ b/android/src/main/java/com/segment/analytics/kotlin/android/plugins/AndroidContextPlugin.kt @@ -24,6 +24,8 @@ import java.util.TimeZone import java.util.UUID import java.lang.System as JavaSystem import android.media.MediaDrm +import com.segment.analytics.kotlin.core.utilities.* +import kotlinx.coroutines.* import java.lang.Exception import java.security.MessageDigest @@ -123,16 +125,53 @@ class AndroidContextPlugin : Plugin { emptyJsonObject } + // use empty string to indicate device id not yet ready + val deviceId = storage.read(Storage.Constants.DeviceId) ?: "" device = buildJsonObject { - put(DEVICE_ID_KEY, getDeviceId(collectDeviceId)) + put(DEVICE_ID_KEY, deviceId) put(DEVICE_MANUFACTURER_KEY, Build.MANUFACTURER) put(DEVICE_MODEL_KEY, Build.MODEL) put(DEVICE_NAME_KEY, Build.DEVICE) put(DEVICE_TYPE_KEY, "android") } + + if (deviceId.isEmpty()) { + loadDeviceId(collectDeviceId) + } + } + + private fun loadDeviceId(collectDeviceId: Boolean) { + // run `getDeviceId` in coroutine, since the DRM API takes a long time + // to generate device id on certain devices and causes ANR issue. + analytics.analyticsScope.launch(analytics.analyticsDispatcher) { + + // generate random identifier that does not persist across installations + // use it as the fallback in case DRM API failed to generate one. + val fallbackDeviceId = UUID.randomUUID().toString() + var deviceId = fallbackDeviceId + + // have to use a different scope than analyticsScope. + // otherwise, timeout cancellation won't work (i.e. the scope can't cancel itself) + val task = CoroutineScope(SupervisorJob()).async { + getDeviceId(collectDeviceId, fallbackDeviceId) + } + + // restrict getDeviceId to 2s to avoid ANR + withTimeoutOrNull(2_000) { + deviceId = task.await() + } + + if (deviceId != fallbackDeviceId) { + device = updateJsonObject(device) { + it[DEVICE_ID_KEY] = deviceId + } + } + + storage.write(Storage.Constants.DeviceId, deviceId) + } } - internal fun getDeviceId(collectDeviceId: Boolean): String { + internal fun getDeviceId(collectDeviceId: Boolean, fallbackDeviceId: String): String { if (!collectDeviceId) { return storage.read(Storage.Constants.AnonymousId) ?: "" } @@ -142,9 +181,8 @@ class AndroidContextPlugin : Plugin { if (!uniqueId.isNullOrEmpty()) { return uniqueId } - // If this still fails, generate random identifier that does not persist across - // installations - return UUID.randomUUID().toString() + // If this still fails, falls back to the random uuid + return fallbackDeviceId } @SuppressLint("MissingPermission") diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidContextCollectorTests.kt b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidContextCollectorTests.kt index 715ac507..a65f7d21 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidContextCollectorTests.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidContextCollectorTests.kt @@ -2,18 +2,19 @@ package com.segment.analytics.kotlin.android import android.content.Context import android.content.SharedPreferences -import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.segment.analytics.kotlin.core.* import com.segment.analytics.kotlin.android.plugins.AndroidContextPlugin import com.segment.analytics.kotlin.android.plugins.getUniqueID import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences +import com.segment.analytics.kotlin.android.utils.testAnalytics import io.mockk.every import io.mockk.mockkStatic import io.mockk.spyk -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.* import kotlinx.serialization.json.* import org.junit.Assert.* +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -24,27 +25,32 @@ import java.util.* @Config(manifest = Config.NONE) class AndroidContextCollectorTests { - val appContext: Context - val analytics: Analytics + lateinit var appContext: Context + lateinit var analytics: Analytics - init { + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { appContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext) val sharedPreferences: SharedPreferences = MemorySharedPreferences() every { appContext.getSharedPreferences(any(), any()) } returns sharedPreferences mockkStatic("com.segment.analytics.kotlin.android.plugins.AndroidContextPluginKt") every { getUniqueID() } returns "unknown" - analytics = Analytics( + analytics = testAnalytics( Configuration( writeKey = "123", application = appContext, storageProvider = AndroidStorageProvider - ) + ), + testScope, testDispatcher ) } @Test - fun `context fields applied correctly`() { + fun `context fields applied correctly`() { // Context of the app under test. analytics.configuration.collectDeviceId = true val contextCollector = AndroidContextPlugin() @@ -101,14 +107,41 @@ class AndroidContextCollectorTests { } @Test - fun `getDeviceId returns anonId when disabled`() = runBlocking { + fun `getDeviceId returns anonId when disabled`() = runTest { analytics.storage.write(Storage.Constants.AnonymousId, "anonId") val contextCollector = AndroidContextPlugin() contextCollector.setup(analytics) - val deviceId = contextCollector.getDeviceId(false) - Log.d("debug flaky test", deviceId) + val deviceId = contextCollector.getDeviceId(false, "") assertEquals(deviceId, "anonId") } + @Test + fun `device id cache is used when presented`() = runTest { + analytics.storage.write(Storage.Constants.DeviceId, "anonId") + + analytics.configuration.collectDeviceId = true + val contextCollector = AndroidContextPlugin() + contextCollector.setup(analytics) + + val event = TrackEvent( + event = "clicked", + properties = buildJsonObject { put("behaviour", "good") }) + .apply { + messageId = "qwerty-1234" + anonymousId = "anonId" + integrations = emptyJsonObject + context = emptyJsonObject + timestamp = Date(0).toInstant().toString() + } + contextCollector.execute(event) + + with(event.context) { + assertTrue(this.containsKey("device")) + this["device"]?.jsonObject?.let { + assertEquals("anonId", it["id"].asString()) + } + } + } + private fun JsonElement?.asString(): String? = this?.jsonPrimitive?.content } \ No newline at end of file diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt index 06409d04..d6298889 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/AndroidLifecyclePluginTests.kt @@ -13,9 +13,11 @@ import com.segment.analytics.kotlin.core.platform.Plugin import com.segment.analytics.kotlin.android.plugins.AndroidLifecycle import com.segment.analytics.kotlin.android.plugins.AndroidLifecyclePlugin import com.segment.analytics.kotlin.android.utils.mockHTTPClient +import com.segment.analytics.kotlin.android.utils.testAnalytics import io.mockk.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.Assert.* @@ -35,6 +37,9 @@ class AndroidLifecyclePluginTests { private lateinit var analytics: Analytics private val mockContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + init { val packageInfo = PackageInfo() packageInfo.versionCode = 100 @@ -56,12 +61,13 @@ class AndroidLifecyclePluginTests { @Before fun setup() { - analytics = Analytics( + analytics = testAnalytics( Configuration( writeKey = "123", application = mockContext, storageProvider = AndroidStorageProvider - ) + ), + testScope, testDispatcher ) } @@ -121,7 +127,7 @@ class AndroidLifecyclePluginTests { } @Test - fun `application opened is tracked`() = runBlocking{ + fun `application opened is tracked`() { analytics.configuration.trackApplicationLifecycleEvents = true analytics.configuration.trackDeepLinks = false analytics.configuration.useLifecycleObserver = false @@ -134,12 +140,10 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - delay(500) lifecyclePlugin.onActivityStarted(mockActivity) - delay(500) lifecyclePlugin.onActivityResumed(mockActivity) - verify (timeout = 2000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val tracks = mutableListOf() verify { mockPlugin.track(capture(tracks)) } assertEquals(2, tracks.size) @@ -169,7 +173,7 @@ class AndroidLifecyclePluginTests { lifecyclePlugin.onActivityStopped(mockActivity) lifecyclePlugin.onActivityDestroyed(mockActivity) - verify (timeout = 2000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { @@ -193,7 +197,7 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - verify (timeout = 4000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { @@ -206,7 +210,7 @@ class AndroidLifecyclePluginTests { } @Test - fun `application updated is tracked`() = runBlocking { + fun `application updated is tracked`() = runTest { analytics.configuration.trackApplicationLifecycleEvents = true analytics.configuration.trackDeepLinks = false analytics.configuration.useLifecycleObserver = false @@ -224,7 +228,7 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - verify (timeout = 2000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { @@ -281,7 +285,7 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - verify (timeout = 2000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { @@ -316,7 +320,7 @@ class AndroidLifecyclePluginTests { // Simulate activity startup lifecyclePlugin.onActivityCreated(mockActivity, mockBundle) - verify (timeout = 4000){ mockPlugin.updateState(true) } + verify { mockPlugin.updateState(true) } val track = slot() verify { mockPlugin.track(capture(track)) } with(track.captured) { diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/EventsFileTests.kt b/android/src/test/java/com/segment/analytics/kotlin/android/EventsFileTests.kt index ad842ae2..65ef815e 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/EventsFileTests.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/EventsFileTests.kt @@ -7,9 +7,8 @@ import com.segment.analytics.kotlin.core.utilities.EncodeDefaultsJson import com.segment.analytics.kotlin.core.utilities.EventsFileManager import io.mockk.every import io.mockk.mockkStatic -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions.* @@ -40,7 +39,7 @@ class EventsFileTests { } @Test - fun `check if event is stored correctly and creates new file`() = runBlocking { + fun `check if event is stored correctly and creates new file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -63,7 +62,7 @@ class EventsFileTests { } @Test - fun `storeEvent stores in existing file if available`() = runBlocking { + fun `storeEvent stores in existing file if available`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -87,7 +86,7 @@ class EventsFileTests { } @Test - fun `storeEvent creates new file when at capacity and closes other file`() = runBlocking { + fun `storeEvent creates new file when at capacity and closes other file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -118,14 +117,14 @@ class EventsFileTests { } @Test - fun `read returns empty list when no events stored`() = runBlocking { + fun `read returns empty list when no events stored`() = runTest { val file = EventsFileManager(directory, "123", kvStore) file.rollover() assertTrue(file.read().isEmpty()) } @Test - fun `read finishes open file and lists it`() = runBlocking { + fun `read finishes open file and lists it`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -151,7 +150,7 @@ class EventsFileTests { } @Test - fun `multiple reads doesnt create extra files`() = runBlocking { + fun `multiple reads doesnt create extra files`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -185,7 +184,7 @@ class EventsFileTests { } @Test - fun `read lists all available files for writekey`() = runBlocking { + fun `read lists all available files for writekey`() = runTest { val trackEvent = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -241,7 +240,7 @@ class EventsFileTests { // } @Test - fun `remove deletes file`() = runBlocking { + fun `remove deletes file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/StorageTests.kt b/android/src/test/java/com/segment/analytics/kotlin/android/StorageTests.kt index cacd6f16..5ef9062a 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/StorageTests.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/StorageTests.kt @@ -5,11 +5,9 @@ import android.content.SharedPreferences import com.segment.analytics.kotlin.core.* import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences import com.segment.analytics.kotlin.android.utils.clearPersistentStorage -import com.segment.analytics.kotlin.android.utils.mockAnalytics import com.segment.analytics.kotlin.android.utils.mockContext -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.serialization.decodeFromString import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -34,7 +32,7 @@ class StorageTests { private var mockContext: Context = mockContext() @BeforeEach - fun setup() = runBlocking { + fun setup() = runTest { clearPersistentStorage() store.provide( UserInfo( @@ -56,14 +54,14 @@ class StorageTests { mockContext, store, "123", - TestCoroutineDispatcher() + UnconfinedTestDispatcher() ) androidStorage.subscribeToStore() } @Test - fun `userInfo update calls write`() = runBlocking { + fun `userInfo update calls write`() = runTest { val action = object : Action { override fun reduce(state: UserInfo): UserInfo { return UserInfo( @@ -84,7 +82,7 @@ class StorageTests { } @Test - fun `system update calls write for settings`() = runBlocking { + fun `system update calls write for settings`() = runTest { val action = object : Action { override fun reduce(state: System): System { return System( @@ -129,7 +127,7 @@ class StorageTests { val map = getWorkingMap(mockContext.getSharedPreferences("", 0)) @Test - fun `write updates sharedPreferences`() = runBlocking { + fun `write updates sharedPreferences`() = runTest { androidStorage.write(Storage.Constants.AppVersion, "100") assertEquals("100", map["segment.app.version"]) } @@ -151,7 +149,7 @@ class StorageTests { inner class EventsStorage() { @Test - fun `writing events writes to eventsFile`() = runBlocking { + fun `writing events writes to eventsFile`() = runTest { val event = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -172,7 +170,7 @@ class StorageTests { } @Test - fun `cannot write more than 32kb as event`() = runBlocking { + fun `cannot write more than 32kb as event`() = runTest { val stringified: String = "A".repeat(32002) val exception = try { androidStorage.write( @@ -190,7 +188,7 @@ class StorageTests { } @Test - fun `reading events returns a non-null file handle with correct events`() = runBlocking { + fun `reading events returns a non-null file handle with correct events`() = runTest { val event = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -224,14 +222,14 @@ class StorageTests { } @Test - fun `reading events with empty storage return empty list`() = runBlocking { + fun `reading events with empty storage return empty list`() = runTest { androidStorage.eventsFile.rollover() val fileUrls = androidStorage.read(Storage.Constants.Events) assertTrue(fileUrls!!.isEmpty()) } @Test - fun `can write and read multiple events`() = runBlocking { + fun `can write and read multiple events`() = runTest { val event1 = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) diff --git a/android/src/test/java/com/segment/analytics/kotlin/android/utils/Mocks.kt b/android/src/test/java/com/segment/analytics/kotlin/android/utils/Mocks.kt index 9871a8dd..209ded04 100644 --- a/android/src/test/java/com/segment/analytics/kotlin/android/utils/Mocks.kt +++ b/android/src/test/java/com/segment/analytics/kotlin/android/utils/Mocks.kt @@ -3,36 +3,36 @@ package com.segment.analytics.kotlin.android.utils import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager -import com.segment.analytics.kotlin.core.Analytics -import com.segment.analytics.kotlin.core.Connection -import com.segment.analytics.kotlin.core.HTTPClient +import com.segment.analytics.kotlin.core.* import io.mockk.every import io.mockk.mockk import io.mockk.mockkConstructor import io.mockk.spyk import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope import sovran.kotlin.Store import java.io.ByteArrayInputStream import java.io.File import java.net.HttpURLConnection import kotlin.coroutines.CoroutineContext -fun mockAnalytics(): Analytics { +fun mockAnalytics(testScope: TestScope, testDispatcher: TestDispatcher): Analytics { val mock = mockk(relaxed = true) - val scope = TestCoroutineScope() - val dispatcher = TestCoroutineDispatcher() - val mockStore = spyStore(scope, dispatcher) + val mockStore = spyStore(testScope, testDispatcher) every { mock.store } returns mockStore - every { mock.analyticsScope } returns scope - every { mock.fileIODispatcher } returns dispatcher - every { mock.networkIODispatcher } returns dispatcher - every { mock.analyticsDispatcher } returns dispatcher + every { mock.analyticsScope } returns testScope + every { mock.fileIODispatcher } returns testDispatcher + every { mock.networkIODispatcher } returns testDispatcher + every { mock.analyticsDispatcher } returns testDispatcher return mock } +fun testAnalytics(configuration: Configuration, testScope: TestScope, testDispatcher: TestDispatcher): Analytics { + return object : Analytics(configuration, TestCoroutineConfiguration(testScope, testDispatcher)) {} +} + fun mockContext(): Context { val mockPrefs = MemorySharedPreferences() val packageInfo = PackageInfo() @@ -72,4 +72,25 @@ fun mockHTTPClient() { val httpConnection: HttpURLConnection = mockk() val connection = object : Connection(httpConnection, settingsStream, null) {} every { anyConstructed().settings("cdn-settings.segment.com/v1") } returns connection +} + +class TestCoroutineConfiguration( + val testScope: TestScope, + val testDispatcher: TestDispatcher +) : CoroutineConfiguration { + + override val store: Store = + spyStore(testScope, testDispatcher) + + override val analyticsScope: CoroutineScope + get() = testScope + + override val analyticsDispatcher: CoroutineDispatcher + get() = testDispatcher + + override val networkIODispatcher: CoroutineDispatcher + get() = testDispatcher + + override val fileIODispatcher: CoroutineDispatcher + get() = testDispatcher } \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index f2ac29a3..1af2ee5e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -17,12 +17,12 @@ dependencies { // MAIN DEPS api 'com.segment:sovran-kotlin:1.2.1' api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' // TESTING repositories { mavenCentral() } testImplementation 'io.mockk:mockk:1.10.6' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' testImplementation platform("org.junit:junit-bom:5.7.2") testImplementation "org.junit.jupiter:junit-jupiter" diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt b/core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt index 107b77b8..e98ad7a8 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt @@ -1,6 +1,5 @@ package com.segment.analytics.kotlin.core -import com.segment.analytics.kotlin.core.platform.DestinationPlugin import com.segment.analytics.kotlin.core.platform.EventPlugin import com.segment.analytics.kotlin.core.platform.Plugin import com.segment.analytics.kotlin.core.platform.Timeline @@ -30,17 +29,27 @@ import kotlin.reflect.KClass * @property networkIODispatcher coroutine dispatcher that runs the network tasks * @property fileIODispatcher coroutine dispatcher that runs the file related tasks */ -class Analytics internal constructor( +open class Analytics protected constructor( val configuration: Configuration, - val store: Store, - val analyticsScope: CoroutineScope = CoroutineScope(SupervisorJob()), - val analyticsDispatcher: CoroutineDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher(), - val networkIODispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(), - val fileIODispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() -) : Subscriber { - - internal val timeline: Timeline - val storage: Storage + coroutineConfig: CoroutineConfiguration +) : Subscriber, CoroutineConfiguration by coroutineConfig { + + // use lazy to avoid the instance being leak before fully initialized + internal val timeline: Timeline by lazy { + Timeline().also { it.analytics = this } + } + + // use lazy to avoid the instance being leak before fully initialized + val storage: Storage by lazy { + configuration.storageProvider.getStorage( + analytics = this, + writeKey = configuration.writeKey, + ioDispatcher = fileIODispatcher, + store = store, + application = configuration.application!! + ) + } + companion object { var debugLogsEnabled: Boolean = false set(value) { @@ -51,15 +60,6 @@ class Analytics internal constructor( init { require(configuration.isValid()) { "invalid configuration" } - timeline = Timeline().also { it.analytics = this } - - storage = configuration.storageProvider.getStorage( - analytics = this, - writeKey = configuration.writeKey, - ioDispatcher = fileIODispatcher, - store = store, - application = configuration.application!! - ) build() } @@ -67,7 +67,13 @@ class Analytics internal constructor( * Public constructor of Analytics. * @property configuration configuration that analytics can use */ - constructor(configuration: Configuration): this(configuration, Store()) + constructor(configuration: Configuration): this(configuration, object : CoroutineConfiguration{ + override val store = Store() + override val analyticsScope = CoroutineScope(SupervisorJob()) + override val analyticsDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher() + override val networkIODispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + override val fileIODispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() + }) // This function provides a default state to the store & attaches the storage and store instances // Initiates the initial call to settings and adds default system plugins diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt b/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt index 7ff1bc70..a70acbd6 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt @@ -4,7 +4,7 @@ import com.segment.analytics.kotlin.core.Constants.DEFAULT_API_HOST import com.segment.analytics.kotlin.core.Constants.DEFAULT_CDN_HOST import com.segment.analytics.kotlin.core.utilities.ConcreteStorageProvider import kotlinx.coroutines.* -import java.util.concurrent.Executors +import sovran.kotlin.Store /** * Configuration that analytics can use @@ -39,4 +39,16 @@ data class Configuration( fun isValid(): Boolean { return writeKey.isNotBlank() && application != null } +} + +interface CoroutineConfiguration { + val store: Store + + val analyticsScope: CoroutineScope + + val analyticsDispatcher: CoroutineDispatcher + + val networkIODispatcher: CoroutineDispatcher + + val fileIODispatcher: CoroutineDispatcher } \ No newline at end of file diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/Storage.kt b/core/src/main/java/com/segment/analytics/kotlin/core/Storage.kt index 40a53eb8..0fb5e4b1 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/Storage.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/Storage.kt @@ -36,7 +36,8 @@ interface Storage { Settings("segment.settings"), Events("segment.events"), AppVersion("segment.app.version"), - AppBuild("segment.app.build") + AppBuild("segment.app.build"), + DeviceId("segment.device.id") } suspend fun subscribeToStore() diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/AnalyticsTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/AnalyticsTests.kt index a9d31eb1..d6e31fdc 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/AnalyticsTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/AnalyticsTests.kt @@ -2,19 +2,15 @@ package com.segment.analytics.kotlin.core import com.segment.analytics.kotlin.core.platform.Plugin import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin -import com.segment.analytics.kotlin.core.utils.StubPlugin -import com.segment.analytics.kotlin.core.utils.TestRunPlugin -import com.segment.analytics.kotlin.core.utils.clearPersistentStorage -import com.segment.analytics.kotlin.core.utils.mockHTTPClient -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.* import io.mockk.every import io.mockk.mockkStatic import io.mockk.slot import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions @@ -34,9 +30,6 @@ import java.util.UUID class AnalyticsTests { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) - private val epochTimestamp = Date(0).toInstant().toString() private val baseContext = buildJsonObject { val lib = buildJsonObject { @@ -46,6 +39,9 @@ class AnalyticsTests { put(ContextPlugin.LIBRARY_KEY, lib) } + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + init { mockkStatic(Instant::class) every { Instant.now() } returns Date(0).toInstant() @@ -62,8 +58,7 @@ class AnalyticsTests { application = "Test" ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) analytics.configuration.autoAddSegmentDestination = false } @@ -215,7 +210,7 @@ class AnalyticsTests { } @Test - fun `identify() overwrites userId and traits`() = runBlocking { + fun `identify() overwrites userId and traits`() = runTest { analytics.store.dispatch( UserInfo.SetUserIdAndTraitsAction( "oldUserId", @@ -275,7 +270,7 @@ class AnalyticsTests { analytics.add(mockPlugin) analytics.group("high school", buildJsonObject { put("foo", "bar") }) val group = slot() - verify (timeout = 2000) { mockPlugin.group(capture(group)) } + verify { mockPlugin.group(capture(group)) } assertEquals( GroupEvent( traits = buildJsonObject { put("foo", "bar") }, @@ -323,7 +318,7 @@ class AnalyticsTests { } @Test - fun `alias event modifies underlying userId`() = runBlocking { + fun `alias event modifies underlying userId`() = runTest { val mockPlugin = spyk(StubPlugin()) analytics.add(mockPlugin) analytics.identify("oldId") @@ -342,7 +337,7 @@ class AnalyticsTests { @Nested inner class Reset { @Test - fun `reset() overwrites userId and traits also resets event plugin`() = runBlocking { + fun `reset() overwrites userId and traits also resets event plugin`() = runTest { val plugin = spyk(StubPlugin()) analytics.add(plugin) @@ -397,7 +392,7 @@ class AnalyticsTests { } @Test - fun `settings fetches current Analytics Settings`() = runBlocking { + fun `settings fetches current Analytics Settings`() = runTest { val settings = Settings( integrations = buildJsonObject { put("int1", true) diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/PluginTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/PluginTests.kt index cd8007aa..6faa0f6c 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/PluginTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/PluginTests.kt @@ -5,6 +5,8 @@ import com.segment.analytics.kotlin.core.platform.Timeline import com.segment.analytics.kotlin.core.utils.mockAnalytics import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions @@ -15,7 +17,9 @@ import java.util.* @TestInstance(TestInstance.Lifecycle.PER_CLASS) class PluginTests { - private val mockAnalytics = mockAnalytics() + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val mockAnalytics = mockAnalytics(testScope, testDispatcher) private val timeline: Timeline init { diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/SettingsTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/SettingsTests.kt index 1510994c..b110cbe7 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/SettingsTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/SettingsTests.kt @@ -4,12 +4,12 @@ import com.segment.analytics.kotlin.core.platform.DestinationPlugin import com.segment.analytics.kotlin.core.platform.Plugin import com.segment.analytics.kotlin.core.utils.StubPlugin import com.segment.analytics.kotlin.core.utils.mockHTTPClient -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.testAnalytics import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -24,8 +24,9 @@ import java.util.concurrent.atomic.AtomicInteger class SettingsTests { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) init { mockHTTPClient() @@ -33,21 +34,16 @@ class SettingsTests { @BeforeEach fun setup() { - analytics = Analytics( - Configuration( - writeKey = "123", - application = "Test" - ), - spyStore(testScope, testDispatcher), - testScope, - testDispatcher, - testDispatcher - ) + + analytics = testAnalytics(Configuration( + writeKey = "123", + application = "Test" + ), testScope, testDispatcher) analytics.configuration.autoAddSegmentDestination = false } @Test - fun `checkSettings updates settings`() = runBlocking { + fun `checkSettings updates settings`() = runTest { val system = analytics.store.currentState(System::class) val curSettings = system?.settings assertEquals( @@ -65,7 +61,7 @@ class SettingsTests { } @Test - fun `settings update updates plugins`() = runBlocking { + fun `settings update updates plugins`() { val mockPlugin = spyk(StubPlugin()) analytics.add(mockPlugin) verify { @@ -149,7 +145,7 @@ class SettingsTests { } @Test - fun `can manually enable destinations`() = runBlocking { + fun `can manually enable destinations`() { val settings = Settings( integrations = buildJsonObject { put("Foo", buildJsonObject { diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/StateTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/StateTest.kt index f4321707..df21362c 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/StateTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/StateTest.kt @@ -2,10 +2,10 @@ package com.segment.analytics.kotlin.core import com.segment.analytics.kotlin.core.utils.clearPersistentStorage import com.segment.analytics.kotlin.core.utils.mockHTTPClient -import com.segment.analytics.kotlin.core.utils.spyStore -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import com.segment.analytics.kotlin.core.utils.testAnalytics +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions.assertEquals @@ -17,8 +17,8 @@ import org.junit.jupiter.api.Test internal class StateTest { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) init { mockHTTPClient() @@ -31,8 +31,7 @@ internal class StateTest { writeKey = "123", application = "Test" ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) analytics.configuration.autoAddSegmentDestination = false } @@ -41,7 +40,7 @@ internal class StateTest { inner class UserInfoTests { @Test - fun resetAction() = runBlocking { + fun resetAction() = runTest { val traits = buildJsonObject { put("behaviour", "bad") } analytics.store.dispatch( UserInfo.SetUserIdAndTraitsAction( @@ -59,7 +58,7 @@ internal class StateTest { } @Test - fun setUserIdAction() = runBlocking { + fun setUserIdAction() = runTest { analytics.store.dispatch(UserInfo.SetUserIdAction("oldUserId"), UserInfo::class) assertEquals("oldUserId", analytics.userId()) @@ -68,13 +67,13 @@ internal class StateTest { } @Test - fun setAnonymousIdAction() = runBlocking { + fun setAnonymousIdAction() = runTest { analytics.store.dispatch(UserInfo.SetAnonymousIdAction("anonymous"), UserInfo::class) assertEquals("anonymous", analytics.store.currentState(UserInfo::class)?.anonymousId) } @Test - fun setTraitsAction() = runBlocking { + fun setTraitsAction() = runTest { val traits = buildJsonObject { put("behaviour", "bad") } analytics.store.dispatch(UserInfo.SetUserIdAction("oldUserId"), UserInfo::class) @@ -86,7 +85,7 @@ internal class StateTest { } @Test - fun setUserIdAndTraitsAction() = runBlocking { + fun setUserIdAndTraitsAction() = runTest { val traits = buildJsonObject { put("behaviour", "bad") } analytics.store.dispatch( UserInfo.SetUserIdAndTraitsAction( diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/compat/JavaAnalyticsTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/compat/JavaAnalyticsTest.kt index 395ba6b1..0b48a445 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/compat/JavaAnalyticsTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/compat/JavaAnalyticsTest.kt @@ -4,14 +4,11 @@ import com.segment.analytics.kotlin.core.* import com.segment.analytics.kotlin.core.platform.DestinationPlugin import com.segment.analytics.kotlin.core.platform.Plugin import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin -import com.segment.analytics.kotlin.core.utils.StubPlugin -import com.segment.analytics.kotlin.core.utils.TestRunPlugin -import com.segment.analytics.kotlin.core.utils.mockHTTPClient -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.* import io.mockk.* -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -28,9 +25,6 @@ internal class JavaAnalyticsTest { private lateinit var analytics: JavaAnalytics private lateinit var mockPlugin: StubPlugin - private val testDispatcher = TestCoroutineDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) - private val epochTimestamp = Date(0).toInstant().toString() private val baseContext = buildJsonObject { val lib = buildJsonObject { @@ -40,6 +34,9 @@ internal class JavaAnalyticsTest { put(ContextPlugin.LIBRARY_KEY, lib) } + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + init { mockkStatic(Instant::class) every { Instant.now() } returns Date(0).toInstant() @@ -55,9 +52,8 @@ internal class JavaAnalyticsTest { .setAutoAddSegmentDestination(false) .build() - val store = spyStore(testScope, testDispatcher) analytics = JavaAnalytics( - Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) ) mockPlugin = spyk(StubPlugin()) } @@ -403,7 +399,7 @@ internal class JavaAnalyticsTest { @Nested inner class Reset { @Test - fun `reset() overwrites userId and traits also resets event plugin`() = runBlocking { + fun `reset() overwrites userId and traits also resets event plugin`() { val plugin = spyk(StubPlugin()) analytics.add(plugin) @@ -427,8 +423,8 @@ internal class JavaAnalyticsTest { .add(testPlugin1) .add(testPlugin2) .process(TrackEvent(event = "track", properties = emptyJsonObject)) - verify(timeout = 2000) { testPlugin1.updateState(true) } - verify(timeout = 2000) { testPlugin2.updateState(true) } + verify { testPlugin1.updateState(true) } + verify { testPlugin2.updateState(true) } } @Test @@ -442,20 +438,20 @@ internal class JavaAnalyticsTest { } @Test - fun userId() = runBlocking { + fun userId() { analytics.identify("userId") assertEquals("userId", analytics.userId()) } @Test - fun traits() = runBlocking { + fun traits() { val json = buildJsonObject { put("name", "bar") } analytics.identify("userId", json) assertEquals(json, analytics.traits()) } @Test - fun settings() = runBlocking { + fun settings() = runTest { val settings = Settings( integrations = buildJsonObject { put("int1", true) diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/DestinationPluginTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/DestinationPluginTests.kt index f8e16331..11e8f0d0 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/DestinationPluginTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/DestinationPluginTests.kt @@ -8,6 +8,8 @@ import com.segment.analytics.kotlin.core.utilities.putInContext import com.segment.analytics.kotlin.core.utils.mockAnalytics import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions.assertEquals @@ -18,7 +20,10 @@ import java.util.Date @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DestinationPluginTests { - private val mockAnalytics = mockAnalytics() + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val mockAnalytics = mockAnalytics(testScope, testDispatcher) private val timeline: Timeline init { diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/EventPipelineTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/EventPipelineTest.kt index b915c81a..80c4f219 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/EventPipelineTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/EventPipelineTest.kt @@ -8,7 +8,9 @@ import com.segment.analytics.kotlin.core.utilities.ConcreteStorageProvider import com.segment.analytics.kotlin.core.utils.mockAnalytics import io.mockk.* import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach @@ -23,13 +25,17 @@ internal class EventPipelineTest { private lateinit var storage: Storage + private val testDispatcher = UnconfinedTestDispatcher() + + private val testScope = TestScope(testDispatcher) + @BeforeEach internal fun setUp() { MockKAnnotations.init(this) mockkConstructor(HTTPClient::class) mockkConstructor(File::class) - analytics = mockAnalytics() + analytics = mockAnalytics(testScope, testDispatcher) storage = spyk(ConcreteStorageProvider.getStorage( analytics, analytics.store, @@ -48,14 +54,14 @@ internal class EventPipelineTest { fun put() { val event = "event 1" pipeline.put(event) - coVerify(timeout = 2000) { storage.write(Storage.Constants.Events, event) } + coVerify { storage.write(Storage.Constants.Events, event) } } @Test fun flush() { pipeline.put("event 1") pipeline.put(EventPipeline.FLUSH_POISON) - coVerify(timeout = 2000) { + coVerify { storage.rollover() storage.read(Storage.Constants.Events) anyConstructed().upload(any()) @@ -78,7 +84,7 @@ internal class EventPipelineTest { fun `put more than flushCount causes flush`() { pipeline.put("event 1") pipeline.put("event 2") - coVerify(timeout = 2000) { + coVerify { storage.rollover() storage.read(Storage.Constants.Events) anyConstructed().upload(any()) @@ -91,7 +97,7 @@ internal class EventPipelineTest { every { anyConstructed().upload(any()) } throws HTTPException(400, "", "") pipeline.put("event 1") pipeline.put("event 2") - coVerify(timeout = 2000) { + coVerify { storage.rollover() storage.read(Storage.Constants.Events) anyConstructed().upload(any()) @@ -104,7 +110,7 @@ internal class EventPipelineTest { every { anyConstructed().upload(any()) } throws HTTPException(300, "", "") pipeline.put("event 1") pipeline.put("event 2") - coVerify(timeout = 2000) { + coVerify { storage.rollover() storage.read(Storage.Constants.Events) anyConstructed().upload(any()) @@ -119,7 +125,7 @@ internal class EventPipelineTest { every { anyConstructed().upload(any()) } throws Exception() pipeline.put("event 1") pipeline.put("event 2") - coVerify(timeout = 2000) { + coVerify { storage.rollover() storage.read(Storage.Constants.Events) anyConstructed().upload(any()) @@ -130,7 +136,7 @@ internal class EventPipelineTest { } @Test - fun `flushInterval causes regular flushing of events`() = runBlocking { + fun `flushInterval causes regular flushing of events`() = runTest { //restart flushScheduler pipeline = EventPipeline(analytics, "test", @@ -147,7 +153,7 @@ internal class EventPipelineTest { } @Test - fun `flush interrupted when no event file exist`() = runBlocking { + fun `flush interrupted when no event file exist`() = runTest { pipeline.put(EventPipeline.FLUSH_POISON) coVerify(exactly = 1) { storage.rollover() diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/DeviceTokenPluginTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/DeviceTokenPluginTests.kt index ff36fa7b..b59de81d 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/DeviceTokenPluginTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/DeviceTokenPluginTests.kt @@ -5,10 +5,10 @@ import com.segment.analytics.kotlin.core.Configuration import com.segment.analytics.kotlin.core.HTTPClient import com.segment.analytics.kotlin.core.TrackEvent import com.segment.analytics.kotlin.core.utils.TestRunPlugin -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.testAnalytics import io.mockk.* -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Assertions.assertEquals @@ -23,10 +23,8 @@ import java.util.* class DeviceTokenPluginTests { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - - // val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - private val testScope = TestCoroutineScope(testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) init { mockkStatic(Instant::class) @@ -43,8 +41,7 @@ class DeviceTokenPluginTests { application = "Test", autoAddSegmentDestination = false ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) } @Test diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/LogTargetTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/LogTargetTest.kt index 6f63d6e2..0a63b97e 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/LogTargetTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/LogTargetTest.kt @@ -4,12 +4,11 @@ import com.segment.analytics.kotlin.core.Analytics import com.segment.analytics.kotlin.core.Configuration import com.segment.analytics.kotlin.core.TrackEvent import com.segment.analytics.kotlin.core.emptyJsonObject -import com.segment.analytics.kotlin.core.utils.spyStore import com.segment.analytics.kotlin.core.platform.plugins.logger.* import com.segment.analytics.kotlin.core.utils.clearPersistentStorage -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope -import org.junit.jupiter.api.Assertions +import com.segment.analytics.kotlin.core.utils.testAnalytics +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach @@ -19,9 +18,8 @@ internal class LogTargetTest { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - - private val testScope = TestCoroutineScope(testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @BeforeEach internal fun setUp() { @@ -31,8 +29,7 @@ internal class LogTargetTest { application = "Tetst", autoAddSegmentDestination = false ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) } @Test diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentDestinationTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentDestinationTests.kt index 97b3ae66..5e901414 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentDestinationTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentDestinationTests.kt @@ -6,11 +6,11 @@ import com.segment.analytics.kotlin.core.utilities.ConcreteStorageProvider import com.segment.analytics.kotlin.core.utilities.EncodeDefaultsJson import com.segment.analytics.kotlin.core.utilities.StorageImpl import com.segment.analytics.kotlin.core.utils.clearPersistentStorage -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.testAnalytics import io.mockk.* -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* @@ -31,15 +31,14 @@ import java.util.concurrent.atomic.AtomicBoolean class SegmentDestinationTests { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - - // val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - private val testScope = TestCoroutineScope(testDispatcher) - private lateinit var segmentDestination: SegmentDestination private val epochTimestamp = Date(0).toInstant().toString() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testScope = TestScope(testDispatcher) + init { mockkStatic(Instant::class) every { Instant.now() } returns Date(0).toInstant() @@ -61,13 +60,12 @@ class SegmentDestinationTests { flushAt = 2, flushInterval = 0 ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + analytics = testAnalytics(config, testScope, testDispatcher) segmentDestination.setup(analytics) } @Test - fun `enqueue adds event to storage`() = runBlocking { + fun `enqueue adds event to storage`() = runTest { val trackEvent = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -128,7 +126,7 @@ class SegmentDestinationTests { analytics.add(testLogger) val destSpy = spyk(segmentDestination) assertEquals(trackEvent, destSpy.track(trackEvent)) - verify(timeout = 2000) { errorAddingPayload.set(true) } + verify { errorAddingPayload.set(true) } } @Test @@ -157,7 +155,7 @@ class SegmentDestinationTests { assertEquals(trackEvent, destSpy.track(trackEvent)) destSpy.flush() - verify(timeout = 2000) { connection.close() } + verify { connection.close() } with(String(outputBytes)) { val contentsJson: JsonObject = Json.decodeFromString(this) assertEquals(2, contentsJson.size) @@ -207,11 +205,11 @@ class SegmentDestinationTests { assertEquals(trackEvent, destSpy.track(trackEvent)) destSpy.flush() - verify(timeout = 2000) { payloadsRejected.set(true) } + verify { payloadsRejected.set(true) } } @Test - fun `flush reads events but does not delete on fail code_429`() = runBlocking { + fun `flush reads events but does not delete on fail code_429`() = runTest { val trackEvent = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -244,7 +242,7 @@ class SegmentDestinationTests { assertEquals(trackEvent, destSpy.track(trackEvent)) destSpy.flush() - verify(timeout = 2000) { errorUploading.set(true) } + verify { errorUploading.set(true) } (analytics.storage as StorageImpl).run { // batch file doesn't get deleted eventsFile.rollover() @@ -253,7 +251,7 @@ class SegmentDestinationTests { } @Test - fun `flush reads events but does not delete on fail code_500`() = runBlocking { + fun `flush reads events but does not delete on fail code_500`() = runTest { val trackEvent = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -287,7 +285,7 @@ class SegmentDestinationTests { assertEquals(trackEvent, destSpy.track(trackEvent)) destSpy.flush() - verify(timeout = 2000) { errorUploading.set(true) } + verify { errorUploading.set(true) } (analytics.storage as StorageImpl).run { // batch file doesn't get deleted eventsFile.rollover() @@ -325,6 +323,6 @@ class SegmentDestinationTests { assertEquals(trackEvent, destSpy.track(trackEvent)) destSpy.flush() - verify(timeout = 2000) { exceptionUploading.set(true) } + verify { exceptionUploading.set(true) } } } \ No newline at end of file diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentLogTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentLogTest.kt index 250c3a94..a601f78c 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentLogTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentLogTest.kt @@ -4,11 +4,11 @@ import com.segment.analytics.kotlin.core.Analytics import com.segment.analytics.kotlin.core.Configuration import com.segment.analytics.kotlin.core.Settings import com.segment.analytics.kotlin.core.platform.Plugin -import com.segment.analytics.kotlin.core.utils.spyStore import com.segment.analytics.kotlin.core.platform.plugins.logger.* import com.segment.analytics.kotlin.core.utils.clearPersistentStorage -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import com.segment.analytics.kotlin.core.utils.testAnalytics +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.AfterEach @@ -21,11 +21,12 @@ internal class SegmentLogTest { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() - - private val testScope = TestCoroutineScope(testDispatcher) private val mockLogger = LoggerMockPlugin() + private val testDispatcher = UnconfinedTestDispatcher() + + private val testScope = TestScope(testDispatcher) + class LoggerMockPlugin : SegmentLog() { var logClosure: ((LogFilterKind, LogMessage) -> Unit)? = null @@ -50,8 +51,8 @@ internal class SegmentLogTest { application = "Test", autoAddSegmentDestination = false ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) + + analytics = testAnalytics(config, testScope, testDispatcher) analytics.add(mockLogger) SegmentLog.loggingEnabled = true } diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt index 066eed25..ce23df1d 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt @@ -3,11 +3,11 @@ package com.segment.analytics.kotlin.core.platform.plugins import com.segment.analytics.kotlin.core.Analytics import com.segment.analytics.kotlin.core.Configuration import com.segment.analytics.kotlin.core.TrackEvent -import com.segment.analytics.kotlin.core.utils.spyStore +import com.segment.analytics.kotlin.core.utils.testAnalytics import io.mockk.every import io.mockk.spyk -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.api.Assertions.* @@ -19,9 +19,9 @@ internal class StartupQueueTest { private lateinit var analytics: Analytics - private val testDispatcher = TestCoroutineDispatcher() + private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) + private val testScope = TestScope(testDispatcher) @BeforeEach internal fun setUp() { @@ -30,9 +30,7 @@ internal class StartupQueueTest { application = "Tetst", autoAddSegmentDestination = false ) - val store = spyStore(testScope, testDispatcher) - analytics = Analytics(config, store, testScope, testDispatcher, testDispatcher, testDispatcher) - analytics = Analytics(config) + analytics = testAnalytics(config, testScope, testDispatcher) } @Test diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/EventsFileManagerTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/EventsFileManagerTest.kt index 7696e87f..85528b5d 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/EventsFileManagerTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/EventsFileManagerTest.kt @@ -4,7 +4,7 @@ import com.segment.analytics.kotlin.core.TrackEvent import com.segment.analytics.kotlin.core.emptyJsonObject import io.mockk.every import io.mockk.mockkStatic -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -32,7 +32,7 @@ internal class EventsFileManagerTest{ } @Test - fun `check if event is stored correctly and creates new file`() = runBlocking { + fun `check if event is stored correctly and creates new file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -55,7 +55,7 @@ internal class EventsFileManagerTest{ } @Test - fun `storeEvent stores in existing file if available`() = runBlocking { + fun `storeEvent stores in existing file if available`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -79,7 +79,7 @@ internal class EventsFileManagerTest{ } @Test - fun `storeEvent creates new file when at capacity and closes other file`() = runBlocking { + fun `storeEvent creates new file when at capacity and closes other file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -116,7 +116,7 @@ internal class EventsFileManagerTest{ } @Test - fun `read finishes open file and lists it`() = runBlocking { + fun `read finishes open file and lists it`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -142,7 +142,7 @@ internal class EventsFileManagerTest{ } @Test - fun `multiple reads doesnt create extra files`() = runBlocking { + fun `multiple reads doesnt create extra files`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", @@ -175,7 +175,7 @@ internal class EventsFileManagerTest{ } @Test - fun `read lists all available files for writekey`() = runBlocking { + fun `read lists all available files for writekey`() = runTest { val trackEvent = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -202,7 +202,7 @@ internal class EventsFileManagerTest{ } @Test - fun `remove deletes file`() = runBlocking { + fun `remove deletes file`() = runTest { val file = EventsFileManager(directory, "123", kvStore) val trackEvent = TrackEvent( event = "clicked", diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/StorageImplTest.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/StorageImplTest.kt index 6b81a5e0..a1f55421 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/StorageImplTest.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/StorageImplTest.kt @@ -3,9 +3,9 @@ package com.segment.analytics.kotlin.core.utilities import com.segment.analytics.kotlin.core.* import com.segment.analytics.kotlin.core.utils.clearPersistentStorage import com.segment.analytics.kotlin.core.utils.spyStore -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* @@ -14,7 +14,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import sovran.kotlin.Action -import sovran.kotlin.Store import java.io.File import java.util.* @@ -22,11 +21,15 @@ internal class StorageImplTest { private val epochTimestamp = Date(0).toInstant().toString() - private var store = spyStore(TestCoroutineScope(), TestCoroutineDispatcher()) + private val testDispatcher = UnconfinedTestDispatcher() + + private val testScope = TestScope(testDispatcher) + + private var store = spyStore(testScope, testDispatcher) private lateinit var storage: StorageImpl @BeforeEach - fun setup() = runBlocking { + fun setup() = runTest { clearPersistentStorage() store.provide( UserInfo( @@ -47,14 +50,14 @@ internal class StorageImplTest { storage = StorageImpl( store, "123", - TestCoroutineDispatcher() + UnconfinedTestDispatcher() ) storage.subscribeToStore() } @Test - fun `userInfo update calls write`() = runBlocking { + fun `userInfo update calls write`() = runTest { val action = object : Action { override fun reduce(state: UserInfo): UserInfo { return UserInfo( @@ -75,7 +78,7 @@ internal class StorageImplTest { } @Test - fun `system update calls write for settings`() = runBlocking { + fun `system update calls write for settings`() = runTest { val action = object : Action { override fun reduce(state: System): System { return System( @@ -119,7 +122,7 @@ internal class StorageImplTest { inner class EventsStorage() { @Test - fun `writing events writes to eventsFile`() = runBlocking { + fun `writing events writes to eventsFile`() = runTest { val event = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -140,7 +143,7 @@ internal class StorageImplTest { } @Test - fun `cannot write more than 32kb as event`() = runBlocking { + fun `cannot write more than 32kb as event`() = runTest { val stringified: String = "A".repeat(32002) val exception = try { storage.write( @@ -157,7 +160,7 @@ internal class StorageImplTest { } @Test - fun `reading events returns a non-null file handle with correct events`() = runBlocking { + fun `reading events returns a non-null file handle with correct events`() = runTest { val event = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -197,7 +200,7 @@ internal class StorageImplTest { } @Test - fun `can write and read multiple events`() = runBlocking { + fun `can write and read multiple events`() = runTest { val event1 = TrackEvent( event = "clicked", properties = buildJsonObject { put("behaviour", "good") }) @@ -250,7 +253,7 @@ internal class StorageImplTest { } @Test - fun remove() = runBlocking { + fun remove() = runTest { val action = object : Action { override fun reduce(state: UserInfo): UserInfo { return UserInfo( diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Mocks.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Mocks.kt index bf9f2123..fee0d934 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Mocks.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Mocks.kt @@ -4,40 +4,38 @@ import com.segment.analytics.kotlin.core.* import io.mockk.* import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope -import sovran.kotlin.State +import kotlinx.coroutines.test.* import sovran.kotlin.Store -import sovran.kotlin.Subscriber import java.io.ByteArrayInputStream import java.io.File import java.net.HttpURLConnection import kotlin.coroutines.CoroutineContext -import kotlin.reflect.KClass /** * Retrieve a relaxed mock of analytics, that can be used while testing plugins * Current capabilities: * - In-memory sovran.store */ -fun mockAnalytics(): Analytics { +fun mockAnalytics(testScope: TestScope, testDispatcher: TestDispatcher): Analytics { val mock = mockk(relaxed = true) - val scope = TestCoroutineScope() - val dispatcher = TestCoroutineDispatcher() - val mockStore = spyStore(scope, dispatcher) + val mockStore = spyStore(testScope, testDispatcher) every { mock.store } returns mockStore - every { mock.analyticsScope } returns scope - every { mock.fileIODispatcher } returns dispatcher - every { mock.networkIODispatcher } returns dispatcher - every { mock.analyticsDispatcher } returns dispatcher + every { mock.analyticsScope } returns testScope + every { mock.fileIODispatcher } returns testDispatcher + every { mock.networkIODispatcher } returns testDispatcher + every { mock.analyticsDispatcher } returns testDispatcher return mock } +fun testAnalytics(configuration: Configuration, testScope: TestScope, testDispatcher: TestDispatcher): Analytics { + return object : Analytics(configuration, TestCoroutineConfiguration(testScope, testDispatcher)) {} +} + fun clearPersistentStorage() { File("/tmp/analytics-kotlin/123").deleteRecursively() } -fun spyStore(scope: CoroutineScope, dispatcher: CoroutineDispatcher): Store { +fun spyStore(scope: TestScope, dispatcher: TestDispatcher): Store { val store = spyk(Store()) every { store getProperty "sovranScope" } propertyType CoroutineScope::class returns scope every { store getProperty "syncQueue" } propertyType CoroutineContext::class returns dispatcher @@ -55,4 +53,24 @@ fun mockHTTPClient() { val httpConnection: HttpURLConnection = mockk() val connection = object : Connection(httpConnection, settingsStream, null) {} every { anyConstructed().settings("cdn-settings.segment.com/v1") } returns connection +} + +class TestCoroutineConfiguration( + val testScope: TestScope, + val testDispatcher: TestDispatcher + ) : CoroutineConfiguration { + + override val store: Store = spyStore(testScope, testDispatcher) + + override val analyticsScope: CoroutineScope + get() = testScope + + override val analyticsDispatcher: CoroutineDispatcher + get() = testDispatcher + + override val networkIODispatcher: CoroutineDispatcher + get() = testDispatcher + + override val fileIODispatcher: CoroutineDispatcher + get() = testDispatcher } \ No newline at end of file diff --git a/samples/kotlin-android-app-destinations/build.gradle b/samples/kotlin-android-app-destinations/build.gradle index 1dc2afce..d9045c2a 100644 --- a/samples/kotlin-android-app-destinations/build.gradle +++ b/samples/kotlin-android-app-destinations/build.gradle @@ -78,7 +78,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' testImplementation 'io.mockk:mockk:1.10.6' testImplementation(platform("org.junit:junit-bom:5.7.2")) diff --git a/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/ComscoreDestinationTests.kt b/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/ComscoreDestinationTests.kt index e4352031..b69f668b 100644 --- a/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/ComscoreDestinationTests.kt +++ b/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/ComscoreDestinationTests.kt @@ -16,8 +16,8 @@ import com.segment.analytics.kotlin.core.utilities.LenientJson import com.segment.analytics.kotlin.core.utilities.getString import io.mockk.* import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -60,9 +60,9 @@ class ComscoreDestinationTests { @MockK lateinit var mockedContext: Context - private val testDispatcher = TestCoroutineDispatcher() + private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestCoroutineScope(testDispatcher) + private val testScope = TestScope(testDispatcher) init { MockKAnnotations.init(this) diff --git a/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/IntercomDestinationTest.kt b/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/IntercomDestinationTest.kt index 8346d3b4..748d85e1 100644 --- a/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/IntercomDestinationTest.kt +++ b/samples/kotlin-android-app-destinations/src/test/java/com/segment/analytics/destinations/plugins/IntercomDestinationTest.kt @@ -4,14 +4,12 @@ import android.app.Application import android.util.Log import com.segment.analytics.kotlin.core.* import com.segment.analytics.kotlin.core.platform.Plugin -import com.segment.analytics.kotlin.core.platform.plugins.logger.* import io.intercom.android.sdk.Company import io.intercom.android.sdk.Intercom import io.intercom.android.sdk.UserAttributes import io.intercom.android.sdk.identity.Registration import io.mockk.* import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.runBlocking import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.* import org.junit.jupiter.api.Assertions.assertEquals @@ -74,7 +72,7 @@ internal class IntercomDestinationTest { } @Test - fun `intercom client not re-initialized when settings is fresh`() = runBlocking { + fun `intercom client not re-initialized when settings is fresh`() { intercomDestination.update(settings, Plugin.UpdateType.Refresh) verify (exactly = 0) { Intercom.initialize(any(), any(), any()) diff --git a/samples/kotlin-android-app/build.gradle b/samples/kotlin-android-app/build.gradle index ffe85444..0a6a9cc0 100644 --- a/samples/kotlin-android-app/build.gradle +++ b/samples/kotlin-android-app/build.gradle @@ -42,8 +42,8 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.0' diff --git a/samples/kotlin-jvm-app/build.gradle b/samples/kotlin-jvm-app/build.gradle index 764bcbcf..23f8caf3 100644 --- a/samples/kotlin-jvm-app/build.gradle +++ b/samples/kotlin-jvm-app/build.gradle @@ -9,7 +9,7 @@ repositories { dependencies { implementation project(':core') - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' } java {