From 83794da6cb979c408afbf66839e74b5e2f5173c1 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Tue, 1 Mar 2022 21:02:01 -0300 Subject: [PATCH 01/13] Add coroutines dependencies to build.gradle --- AutomatticTracks/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index 420c1d26..8ef51924 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -21,6 +21,10 @@ dependencies { testImplementation 'org.mockito.kotlin:mockito-kotlin:2.2.11' testImplementation 'org.mockito:mockito-inline:3.8.0' + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" + androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test:runner:1.3.0' } From ffcaa7c70255019b95c9b2e7a6224bbef2aa9894 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 2 Mar 2022 12:15:02 -0300 Subject: [PATCH 02/13] Add FluxC dependency to build.gradle --- AutomatticTracks/build.gradle | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index 8ef51924..296b5c28 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -6,7 +6,20 @@ plugins { repositories { google() - mavenCentral() + maven { + url 'https://a8c-libs.s3.amazonaws.com/android' + content { + includeGroup "org.wordpress" + includeGroup "org.wordpress.fluxc" + includeGroup "org.wordpress.wellsql" + } + } + maven { + url "https://a8c-libs.s3.amazonaws.com/android/jcenter-mirror" + content { + includeVersion "com.android.volley", "volley", "1.1.1" + } + } } dependencies { @@ -23,8 +36,11 @@ dependencies { // Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" + //FluxC + implementation "org.wordpress:fluxc:1.36.0" + + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test:runner:1.3.0' } From 6988b9305930d2cb5dee31f9531a6fa47b0f9ce8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 2 Mar 2022 15:08:22 -0300 Subject: [PATCH 03/13] Add WordPress utils dependency to build.gradle --- AutomatticTracks/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index 296b5c28..ee142fa4 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -40,6 +40,9 @@ dependencies { //FluxC implementation "org.wordpress:fluxc:1.36.0" + //WordPress utils + implementation "org.wordpress:utils:2.3.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test:runner:1.3.0' From 74be9c150b62e743906e58b70412a61eff1f1e6a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Wed, 2 Mar 2022 15:10:57 -0300 Subject: [PATCH 04/13] Add ExPlat classes (copied from WordPress-Android project) --- .../android/tracks/experiment/ExPlat.kt | 86 +++++++ .../android/tracks/experiment/Experiment.kt | 12 + .../experiment/example/ExampleExperiment.kt | 10 + .../android/tracks/experiment/ExPlatTest.kt | 229 ++++++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt create mode 100644 AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt create mode 100644 AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt create mode 100644 AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt new file mode 100644 index 00000000..f9281010 --- /dev/null +++ b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt @@ -0,0 +1,86 @@ +package com.automattic.android.tracks.experiment + +import com.automattic.android.tracks.experiment.ExPlat.RefreshStrategy.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.experiments.Assignments +import org.wordpress.android.fluxc.model.experiments.Variation +import org.wordpress.android.fluxc.model.experiments.Variation.Control +import org.wordpress.android.fluxc.store.ExperimentStore +import org.wordpress.android.fluxc.store.ExperimentStore.Platform +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.util.AppLog.T + +class ExPlat( + private val experiments: Set, + private val experimentStore: ExperimentStore, + private val appLogWrapper: AppLogWrapper, + private val coroutineScope: CoroutineScope, + private val isDebug: Boolean +) { + private val platform = Platform.WORDPRESS_ANDROID + private val activeVariations = mutableMapOf() + private val experimentNames: List by lazy { experiments.map { it.name } } + + fun refreshIfNeeded() { + refresh(refreshStrategy = IF_STALE) + } + + fun forceRefresh() { + refresh(refreshStrategy = ALWAYS) + } + + fun clear() { + appLogWrapper.d(T.API, "ExPlat: clearing cached assignments and active variations") + activeVariations.clear() + experimentStore.clearCachedAssignments() + } + + /** + * This returns the current active [Variation] for the provided [Experiment]. + * + * If no active [Variation] is found, we can assume this is the first time this method is being + * called for the provided [Experiment] during the current session. In this case, the [Variation] + * is returned from the cached [Assignments] and then set as active. If the cached [Assignments] + * is stale and [shouldRefreshIfStale] is `true`, then new [Assignments] are fetched and their + * variations are going to be returned by this method on the next session. + * + * If the provided [Experiment] was not included in [ExPlat.start], then [Control] is returned. + * If [BuildConfig.DEBUG] is `true`, an [IllegalArgumentException] is thrown instead. + */ + internal fun getVariation(experiment: Experiment, shouldRefreshIfStale: Boolean): Variation { + if (!experimentNames.contains(experiment.name)) { + val message = "ExPlat: experiment not found: \"${experiment.name}\"! " + + "Make sure to include it in the set provided via constructor." + appLogWrapper.e(T.API, message) + if (isDebug) throw IllegalArgumentException(message) else return Control + } + return activeVariations.getOrPut(experiment.name) { + getAssignments(if (shouldRefreshIfStale) IF_STALE else NEVER).getVariationForExperiment(experiment.name) + } + } + + private fun refresh(refreshStrategy: RefreshStrategy) { + if (experimentNames.isNotEmpty()) { + getAssignments(refreshStrategy) + } + } + + private fun getAssignments(refreshStrategy: RefreshStrategy): Assignments { + val cachedAssignments = experimentStore.getCachedAssignments() ?: Assignments() + if (refreshStrategy == ALWAYS || (refreshStrategy == IF_STALE && cachedAssignments.isStale())) { + coroutineScope.launch { fetchAssignments() } + } + return cachedAssignments + } + + private suspend fun fetchAssignments() = experimentStore.fetchAssignments(platform, experimentNames).also { + if (it.isError) { + appLogWrapper.d(T.API, "ExPlat: fetching assignments failed with result: ${it.error}") + } else { + appLogWrapper.d(T.API, "ExPlat: fetching assignments successful with result: ${it.assignments}") + } + } + + private enum class RefreshStrategy { ALWAYS, IF_STALE, NEVER } +} diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt new file mode 100644 index 00000000..f6471633 --- /dev/null +++ b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt @@ -0,0 +1,12 @@ +package com.automattic.android.tracks.experiment + +import org.wordpress.android.fluxc.model.experiments.Variation + +abstract class Experiment( + val name: String, + private val exPlat: ExPlat +) { + @JvmOverloads fun getVariation(shouldRefreshIfStale: Boolean = false): Variation { + return exPlat.getVariation(this, shouldRefreshIfStale) + } +} diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt new file mode 100644 index 00000000..d7ac6bdc --- /dev/null +++ b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt @@ -0,0 +1,10 @@ +package com.automattic.android.tracks.experiment.example + +import com.automattic.android.tracks.experiment.ExPlat +import com.automattic.android.tracks.experiment.Experiment + +@Suppress("unused") +class ExampleExperiment(exPlat: ExPlat) : Experiment( + name = "example_experiment", + exPlat = exPlat +) diff --git a/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt b/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt new file mode 100644 index 00000000..1709afed --- /dev/null +++ b/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt @@ -0,0 +1,229 @@ +package com.automattic.android.tracks.experiment + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.* +import org.wordpress.android.fluxc.model.experiments.Assignments +import org.wordpress.android.fluxc.model.experiments.Variation +import org.wordpress.android.fluxc.model.experiments.Variation.Control +import org.wordpress.android.fluxc.model.experiments.Variation.Treatment +import org.wordpress.android.fluxc.store.ExperimentStore +import org.wordpress.android.fluxc.store.ExperimentStore.OnAssignmentsFetched +import org.wordpress.android.fluxc.utils.AppLogWrapper +import java.util.Date + +@ExperimentalCoroutinesApi +class ExPlatTest { + private val experiments: Set = mock() + private val experimentStore: ExperimentStore = mock() + private val appLogWrapper: AppLogWrapper = mock() + private lateinit var exPlat: ExPlat + private lateinit var dummyExperiment: Experiment + + @Before + fun setUp() { + exPlat = createExPlat(isDebug = false) + dummyExperiment = object : Experiment("dummy", exPlat) {} + + setupExperiments(setOf(dummyExperiment)) + } + + @Test + fun `refreshIfNeeded fetches assignments if cache is null`() = runTest { + setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) + + exPlat.refreshIfNeeded() + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `refreshIfNeeded fetches assignments if cache is stale`() = runTest { + setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) + + exPlat.refreshIfNeeded() + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `refreshIfNeeded does not fetch assignments if cache is fresh`() = runTest { + setupAssignments(cachedAssignments = buildAssignments(isStale = false), fetchedAssignments = buildAssignments()) + + exPlat.refreshIfNeeded() + + verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `forceRefresh fetches assignments if cache is fresh`() = runTest { + setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) + + exPlat.forceRefresh() + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `clear calls experiment store`() = runTest { + exPlat.clear() + + verify(experimentStore, times(1)).clearCachedAssignments() + } + + @Test + fun `getVariation fetches assignments if cache is null`() = runTest { + setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) + + exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `getVariation fetches assignments if cache is stale`() = runTest { + setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) + + exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `getVariation does not fetch assignments if cache is fresh`() = runTest { + setupAssignments(cachedAssignments = buildAssignments(isStale = false), fetchedAssignments = buildAssignments()) + + exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + + verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `getVariation does not fetch assignments if cache is null but shouldRefreshIfStale is false`() = runTest { + setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) + + exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + + verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `getVariation does not fetch assignments if cache is stale but shouldRefreshIfStale is false`() = runTest { + setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) + + exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + + verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `getVariation does not return different cached assignments if active variation exists`() = runTest { + val controlVariation = Control + val treatmentVariation = Treatment("treatment") + + val treatmentAssignments = buildAssignments(variations = mapOf(dummyExperiment.name to treatmentVariation)) + + setupAssignments(cachedAssignments = null, fetchedAssignments = treatmentAssignments) + + val firstVariation = exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + assertThat(firstVariation).isEqualTo(controlVariation) + + exPlat.forceRefresh() + + setupAssignments(cachedAssignments = treatmentAssignments, fetchedAssignments = treatmentAssignments) + + val secondVariation = exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + assertThat(secondVariation).isEqualTo(controlVariation) + } + + @Test + fun `forceRefresh fetches assignments if experiments is not empty`() = runTest { + setupExperiments(setOf(dummyExperiment)) + + exPlat.forceRefresh() + + verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + } + + @Test + fun `forceRefresh does not interact with store if experiments is empty`() = runTest { + setupExperiments(emptySet()) + + exPlat.forceRefresh() + + verifyZeroInteractions(experimentStore) + } + + @Test + fun `refreshIfNeeded does not interact with store if experiments is empty`() = runTest { + setupExperiments(emptySet()) + + exPlat.refreshIfNeeded() + + verifyZeroInteractions(experimentStore) + } + + @Test + fun `getVariation does not interact with store if experiments is empty`() = runTest { + setupExperiments(emptySet()) + + try { + exPlat.getVariation(dummyExperiment, false) + } catch (e: IllegalArgumentException) { + // Do nothing. + } finally { + verifyZeroInteractions(experimentStore) + } + } + + @Test(expected = IllegalArgumentException::class) + fun `getVariation throws IllegalArgumentException if experiment was not found and is debug`() { + runTest { + val exPlatDebug = createExPlat(isDebug = true) + setupExperiments(emptySet()) + exPlatDebug.getVariation(dummyExperiment, false) + } + } + + private fun createExPlat(isDebug: Boolean): ExPlat = + ExPlat( + experiments = experiments, + experimentStore = experimentStore, + appLogWrapper = appLogWrapper, + coroutineScope = CoroutineScope(Dispatchers.Unconfined), + isDebug = isDebug + ) + + private fun setupExperiments(experiments: Set) { + whenever(this.experiments).thenReturn(experiments) + } + + private suspend fun setupAssignments(cachedAssignments: Assignments?, fetchedAssignments: Assignments) { + whenever(experimentStore.getCachedAssignments()).thenReturn(cachedAssignments) + whenever(experimentStore.fetchAssignments(any(), any(), anyOrNull())) + .thenReturn(OnAssignmentsFetched(fetchedAssignments)) + } + + private fun buildAssignments( + isStale: Boolean = false, + variations: Map = emptyMap() + ): Assignments { + val now = System.currentTimeMillis() + val oneHourAgo = now - ONE_HOUR_IN_SECONDS * 1000 + val oneHourFromNow = now + ONE_HOUR_IN_SECONDS * 1000 + return if (isStale) { + Assignments(variations, ONE_HOUR_IN_SECONDS, Date(oneHourAgo)) + } else { + Assignments(variations, ONE_HOUR_IN_SECONDS, Date(oneHourFromNow)) + } + } + + companion object { + private const val ONE_HOUR_IN_SECONDS = 3600 + } +} From 205256ea42372853bbd17397ec8fc774f2b36d1b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Thu, 3 Mar 2022 17:18:47 -0300 Subject: [PATCH 05/13] Update ExPlat dependencies versions, ExPlat unit tests --- AutomatticTracks/build.gradle | 4 +- .../android/tracks/experiment/ExPlatTest.kt | 97 ++++++++++--------- sampletracksapp/build.gradle | 3 + 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index ee142fa4..b635c3a9 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -35,7 +35,7 @@ dependencies { testImplementation 'org.mockito:mockito-inline:3.8.0' // Coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" //FluxC implementation "org.wordpress:fluxc:1.36.0" @@ -43,7 +43,7 @@ dependencies { //WordPress utils implementation "org.wordpress:utils:2.3.0" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test:runner:1.3.0' } diff --git a/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt b/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt index 1709afed..c5c89d6d 100644 --- a/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt +++ b/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt @@ -3,9 +3,8 @@ package com.automattic.android.tracks.experiment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.runBlockingTest import org.assertj.core.api.Assertions.assertThat -import org.junit.Before import org.junit.Test import org.mockito.kotlin.* import org.wordpress.android.fluxc.model.experiments.Assignments @@ -19,22 +18,20 @@ import java.util.Date @ExperimentalCoroutinesApi class ExPlatTest { - private val experiments: Set = mock() private val experimentStore: ExperimentStore = mock() private val appLogWrapper: AppLogWrapper = mock() - private lateinit var exPlat: ExPlat - private lateinit var dummyExperiment: Experiment - - @Before - fun setUp() { - exPlat = createExPlat(isDebug = false) - dummyExperiment = object : Experiment("dummy", exPlat) {} - - setupExperiments(setOf(dummyExperiment)) - } + private var exPlat: ExPlat = createExPlat( + isDebug = false, + experiments = emptySet() + ) + private val dummyExperiment = object : Experiment("dummy", exPlat) {} @Test - fun `refreshIfNeeded fetches assignments if cache is null`() = runTest { + fun `refreshIfNeeded fetches assignments if cache is null`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) exPlat.refreshIfNeeded() @@ -43,7 +40,11 @@ class ExPlatTest { } @Test - fun `refreshIfNeeded fetches assignments if cache is stale`() = runTest { + fun `refreshIfNeeded fetches assignments if cache is stale`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) exPlat.refreshIfNeeded() @@ -52,7 +53,7 @@ class ExPlatTest { } @Test - fun `refreshIfNeeded does not fetch assignments if cache is fresh`() = runTest { + fun `refreshIfNeeded does not fetch assignments if cache is fresh`() = runBlockingTest { setupAssignments(cachedAssignments = buildAssignments(isStale = false), fetchedAssignments = buildAssignments()) exPlat.refreshIfNeeded() @@ -61,7 +62,11 @@ class ExPlatTest { } @Test - fun `forceRefresh fetches assignments if cache is fresh`() = runTest { + fun `forceRefresh fetches assignments if cache is fresh`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) exPlat.forceRefresh() @@ -70,14 +75,18 @@ class ExPlatTest { } @Test - fun `clear calls experiment store`() = runTest { + fun `clear calls experiment store`() = runBlockingTest { exPlat.clear() verify(experimentStore, times(1)).clearCachedAssignments() } @Test - fun `getVariation fetches assignments if cache is null`() = runTest { + fun `getVariation fetches assignments if cache is null`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) @@ -86,7 +95,11 @@ class ExPlatTest { } @Test - fun `getVariation fetches assignments if cache is stale`() = runTest { + fun `getVariation fetches assignments if cache is stale`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) @@ -95,7 +108,7 @@ class ExPlatTest { } @Test - fun `getVariation does not fetch assignments if cache is fresh`() = runTest { + fun `getVariation does not fetch assignments if cache is fresh`() = runBlockingTest { setupAssignments(cachedAssignments = buildAssignments(isStale = false), fetchedAssignments = buildAssignments()) exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) @@ -104,7 +117,7 @@ class ExPlatTest { } @Test - fun `getVariation does not fetch assignments if cache is null but shouldRefreshIfStale is false`() = runTest { + fun `getVariation does not fetch assignments if cache is null but shouldRefreshIfStale is false`() = runBlockingTest { setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) @@ -113,7 +126,7 @@ class ExPlatTest { } @Test - fun `getVariation does not fetch assignments if cache is stale but shouldRefreshIfStale is false`() = runTest { + fun `getVariation does not fetch assignments if cache is stale but shouldRefreshIfStale is false`() = runBlockingTest { setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) @@ -122,7 +135,7 @@ class ExPlatTest { } @Test - fun `getVariation does not return different cached assignments if active variation exists`() = runTest { + fun `getVariation does not return different cached assignments if active variation exists`() = runBlockingTest { val controlVariation = Control val treatmentVariation = Treatment("treatment") @@ -142,36 +155,32 @@ class ExPlatTest { } @Test - fun `forceRefresh fetches assignments if experiments is not empty`() = runTest { - setupExperiments(setOf(dummyExperiment)) - + fun `forceRefresh fetches assignments if experiments is not empty`() = runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = setOf(dummyExperiment) + ) exPlat.forceRefresh() verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) } @Test - fun `forceRefresh does not interact with store if experiments is empty`() = runTest { - setupExperiments(emptySet()) - + fun `forceRefresh does not interact with store if experiments is empty`() = runBlockingTest { exPlat.forceRefresh() verifyZeroInteractions(experimentStore) } @Test - fun `refreshIfNeeded does not interact with store if experiments is empty`() = runTest { - setupExperiments(emptySet()) - + fun `refreshIfNeeded does not interact with store if experiments is empty`() = runBlockingTest { exPlat.refreshIfNeeded() verifyZeroInteractions(experimentStore) } @Test - fun `getVariation does not interact with store if experiments is empty`() = runTest { - setupExperiments(emptySet()) - + fun `getVariation does not interact with store if experiments is empty`() = runBlockingTest { try { exPlat.getVariation(dummyExperiment, false) } catch (e: IllegalArgumentException) { @@ -183,14 +192,16 @@ class ExPlatTest { @Test(expected = IllegalArgumentException::class) fun `getVariation throws IllegalArgumentException if experiment was not found and is debug`() { - runTest { - val exPlatDebug = createExPlat(isDebug = true) - setupExperiments(emptySet()) - exPlatDebug.getVariation(dummyExperiment, false) + runBlockingTest { + exPlat = createExPlat( + isDebug = true, + experiments = emptySet() + ) + exPlat.getVariation(dummyExperiment, false) } } - private fun createExPlat(isDebug: Boolean): ExPlat = + private fun createExPlat(isDebug: Boolean, experiments: Set): ExPlat = ExPlat( experiments = experiments, experimentStore = experimentStore, @@ -199,10 +210,6 @@ class ExPlatTest { isDebug = isDebug ) - private fun setupExperiments(experiments: Set) { - whenever(this.experiments).thenReturn(experiments) - } - private suspend fun setupAssignments(cachedAssignments: Assignments?, fetchedAssignments: Assignments) { whenever(experimentStore.getCachedAssignments()).thenReturn(cachedAssignments) whenever(experimentStore.fetchAssignments(any(), any(), anyOrNull())) diff --git a/sampletracksapp/build.gradle b/sampletracksapp/build.gradle index 859b6f97..be0358b9 100644 --- a/sampletracksapp/build.gradle +++ b/sampletracksapp/build.gradle @@ -5,6 +5,9 @@ plugins { repositories { google() + maven { + url 'https://a8c-libs.s3.amazonaws.com/android' + } } android { From d1fe920faa303a065d1c4319c696ee24ae47205d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 02:43:48 -0300 Subject: [PATCH 06/13] Extract ExPlat implementation to :experimentation module --- AutomatticTracks/build.gradle | 24 -------- .../experiment/example/ExampleExperiment.kt | 10 ---- experimentation/.gitignore | 1 + experimentation/build.gradle | 58 +++++++++++++++++++ experimentation/consumer-rules.pro | 0 experimentation/proguard-rules.pro | 21 +++++++ .../ExampleInstrumentedTest.kt | 24 ++++++++ experimentation/src/main/AndroidManifest.xml | 5 ++ .../android/experimentation}/ExPlat.kt | 10 ++-- .../android/experimentation}/Experiment.kt | 2 +- .../example/ExampleExperiment.kt | 10 ++++ .../android/experimentation}/ExPlatTest.kt | 2 +- settings.gradle | 1 + 13 files changed, 128 insertions(+), 40 deletions(-) delete mode 100644 AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt create mode 100644 experimentation/.gitignore create mode 100644 experimentation/build.gradle create mode 100644 experimentation/consumer-rules.pro create mode 100644 experimentation/proguard-rules.pro create mode 100644 experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt create mode 100644 experimentation/src/main/AndroidManifest.xml rename {AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment => experimentation/src/main/java/com/automattic/android/experimentation}/ExPlat.kt (88%) rename {AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment => experimentation/src/main/java/com/automattic/android/experimentation}/Experiment.kt (86%) create mode 100644 experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt rename {AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment => experimentation/src/test/java/com/automattic/android/experimentation}/ExPlatTest.kt (99%) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index b635c3a9..19707d1b 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -6,20 +6,6 @@ plugins { repositories { google() - maven { - url 'https://a8c-libs.s3.amazonaws.com/android' - content { - includeGroup "org.wordpress" - includeGroup "org.wordpress.fluxc" - includeGroup "org.wordpress.wellsql" - } - } - maven { - url "https://a8c-libs.s3.amazonaws.com/android/jcenter-mirror" - content { - includeVersion "com.android.volley", "volley", "1.1.1" - } - } } dependencies { @@ -34,16 +20,6 @@ dependencies { testImplementation 'org.mockito.kotlin:mockito-kotlin:2.2.11' testImplementation 'org.mockito:mockito-inline:3.8.0' - // Coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" - - //FluxC - implementation "org.wordpress:fluxc:1.36.0" - - //WordPress utils - implementation "org.wordpress:utils:2.3.0" - - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test:runner:1.3.0' } diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt b/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt deleted file mode 100644 index d7ac6bdc..00000000 --- a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/example/ExampleExperiment.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.automattic.android.tracks.experiment.example - -import com.automattic.android.tracks.experiment.ExPlat -import com.automattic.android.tracks.experiment.Experiment - -@Suppress("unused") -class ExampleExperiment(exPlat: ExPlat) : Experiment( - name = "example_experiment", - exPlat = exPlat -) diff --git a/experimentation/.gitignore b/experimentation/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/experimentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/experimentation/build.gradle b/experimentation/build.gradle new file mode 100644 index 00000000..d4850fbf --- /dev/null +++ b/experimentation/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +repositories { + google() + maven { + url 'https://a8c-libs.s3.amazonaws.com/android' + content { + includeGroup "org.wordpress" + includeGroup "org.wordpress.fluxc" + includeGroup "org.wordpress.wellsql" + } + } +} + +android { + compileSdkVersion 32 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" + + //FluxC + implementation "org.wordpress:fluxc:1.36.0" + + //WordPress utils + implementation "org.wordpress:utils:2.3.0" + + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.19.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:2.2.11' + testImplementation 'org.mockito:mockito-inline:3.8.0' +} \ No newline at end of file diff --git a/experimentation/consumer-rules.pro b/experimentation/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/experimentation/proguard-rules.pro b/experimentation/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/experimentation/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt b/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..855f14c9 --- /dev/null +++ b/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.automattic.android.experimentation + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.automattic.android.experiment.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/experimentation/src/main/AndroidManifest.xml b/experimentation/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c84ce677 --- /dev/null +++ b/experimentation/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt similarity index 88% rename from AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt rename to experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt index f9281010..5beff7f0 100644 --- a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/ExPlat.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt @@ -1,6 +1,8 @@ -package com.automattic.android.tracks.experiment +package com.automattic.android.experimentation -import com.automattic.android.tracks.experiment.ExPlat.RefreshStrategy.* +import com.automattic.android.experimentation.ExPlat.RefreshStrategy.ALWAYS +import com.automattic.android.experimentation.ExPlat.RefreshStrategy.IF_STALE +import com.automattic.android.experimentation.ExPlat.RefreshStrategy.NEVER import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.wordpress.android.fluxc.model.experiments.Assignments @@ -45,8 +47,8 @@ class ExPlat( * is stale and [shouldRefreshIfStale] is `true`, then new [Assignments] are fetched and their * variations are going to be returned by this method on the next session. * - * If the provided [Experiment] was not included in [ExPlat.start], then [Control] is returned. - * If [BuildConfig.DEBUG] is `true`, an [IllegalArgumentException] is thrown instead. + * If the provided [Experiment] was not included in [experiments], then [Control] is returned. + * If [isDebug] is `true`, an [IllegalArgumentException] is thrown instead. */ internal fun getVariation(experiment: Experiment, shouldRefreshIfStale: Boolean): Variation { if (!experimentNames.contains(experiment.name)) { diff --git a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt similarity index 86% rename from AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt rename to experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt index f6471633..a2eed50b 100644 --- a/AutomatticTracks/src/main/java/com/automattic/android/tracks/experiment/Experiment.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt @@ -1,4 +1,4 @@ -package com.automattic.android.tracks.experiment +package com.automattic.android.experimentation import org.wordpress.android.fluxc.model.experiments.Variation diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt new file mode 100644 index 00000000..41c6cf12 --- /dev/null +++ b/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt @@ -0,0 +1,10 @@ +package com.automattic.android.experimentation.example + +import com.automattic.android.experimentation.ExPlat +import com.automattic.android.experimentation.Experiment + +@Suppress("unused") +class ExampleExperiment(exPlat: ExPlat) : Experiment( + name = "example_experiment", + exPlat = exPlat +) diff --git a/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt similarity index 99% rename from AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt rename to experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt index c5c89d6d..bef8c81a 100644 --- a/AutomatticTracks/src/test/java/com/automattic/android/tracks/experiment/ExPlatTest.kt +++ b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt @@ -1,4 +1,4 @@ -package com.automattic.android.tracks.experiment +package com.automattic.android.experimentation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/settings.gradle b/settings.gradle index dd3fe524..d3d1f644 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,4 @@ pluginManagement { include ':AutomatticTracks' include ':sampletracksapp' +include ':experimentation' From 7a9751a65ee2feb41db7a80af2b7e9ab9c95264a Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 02:59:04 -0300 Subject: [PATCH 07/13] Pass experiment name to ExPlat.getVariation instead of Experiment instance --- .../android/experimentation/ExPlat.kt | 11 ++++---- .../android/experimentation/Experiment.kt | 15 +++++++---- .../android/experimentation/ExPlatTest.kt | 27 ++++++++++++------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt index 5beff7f0..0efad766 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt @@ -50,15 +50,16 @@ class ExPlat( * If the provided [Experiment] was not included in [experiments], then [Control] is returned. * If [isDebug] is `true`, an [IllegalArgumentException] is thrown instead. */ - internal fun getVariation(experiment: Experiment, shouldRefreshIfStale: Boolean): Variation { - if (!experimentNames.contains(experiment.name)) { - val message = "ExPlat: experiment not found: \"${experiment.name}\"! " + + internal fun getVariation(experimentName: String, shouldRefreshIfStale: Boolean): Variation { + if (!experimentNames.contains(experimentName)) { + val message = "ExPlat: experiment not found: \"${experimentName}\"! " + "Make sure to include it in the set provided via constructor." appLogWrapper.e(T.API, message) if (isDebug) throw IllegalArgumentException(message) else return Control } - return activeVariations.getOrPut(experiment.name) { - getAssignments(if (shouldRefreshIfStale) IF_STALE else NEVER).getVariationForExperiment(experiment.name) + return activeVariations.getOrPut(experimentName) { + getAssignments(if (shouldRefreshIfStale) IF_STALE else NEVER) + .getVariationForExperiment(experimentName) } } diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt index a2eed50b..689a9ffe 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt @@ -2,11 +2,16 @@ package com.automattic.android.experimentation import org.wordpress.android.fluxc.model.experiments.Variation -abstract class Experiment( - val name: String, - private val exPlat: ExPlat +open class Experiment( + val name: String, + private val exPlat: ExPlat ) { - @JvmOverloads fun getVariation(shouldRefreshIfStale: Boolean = false): Variation { - return exPlat.getVariation(this, shouldRefreshIfStale) + @Suppress("unused") + @JvmOverloads + fun getVariation(shouldRefreshIfStale: Boolean = false): Variation { + return exPlat.getVariation( + experimentName = name, + shouldRefreshIfStale = shouldRefreshIfStale + ) } } diff --git a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt index bef8c81a..7aa26bc6 100644 --- a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt +++ b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt @@ -6,7 +6,14 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import org.mockito.kotlin.* +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.model.experiments.Assignments import org.wordpress.android.fluxc.model.experiments.Variation import org.wordpress.android.fluxc.model.experiments.Variation.Control @@ -89,7 +96,7 @@ class ExPlatTest { ) setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) - exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) } @@ -102,7 +109,7 @@ class ExPlatTest { ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) - exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) } @@ -111,7 +118,7 @@ class ExPlatTest { fun `getVariation does not fetch assignments if cache is fresh`() = runBlockingTest { setupAssignments(cachedAssignments = buildAssignments(isStale = false), fetchedAssignments = buildAssignments()) - exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = true) + exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) } @@ -120,7 +127,7 @@ class ExPlatTest { fun `getVariation does not fetch assignments if cache is null but shouldRefreshIfStale is false`() = runBlockingTest { setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) - exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) } @@ -129,7 +136,7 @@ class ExPlatTest { fun `getVariation does not fetch assignments if cache is stale but shouldRefreshIfStale is false`() = runBlockingTest { setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) - exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) } @@ -143,14 +150,14 @@ class ExPlatTest { setupAssignments(cachedAssignments = null, fetchedAssignments = treatmentAssignments) - val firstVariation = exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + val firstVariation = exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) assertThat(firstVariation).isEqualTo(controlVariation) exPlat.forceRefresh() setupAssignments(cachedAssignments = treatmentAssignments, fetchedAssignments = treatmentAssignments) - val secondVariation = exPlat.getVariation(dummyExperiment, shouldRefreshIfStale = false) + val secondVariation = exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) assertThat(secondVariation).isEqualTo(controlVariation) } @@ -182,7 +189,7 @@ class ExPlatTest { @Test fun `getVariation does not interact with store if experiments is empty`() = runBlockingTest { try { - exPlat.getVariation(dummyExperiment, false) + exPlat.getVariation(dummyExperiment.name, false) } catch (e: IllegalArgumentException) { // Do nothing. } finally { @@ -197,7 +204,7 @@ class ExPlatTest { isDebug = true, experiments = emptySet() ) - exPlat.getVariation(dummyExperiment, false) + exPlat.getVariation(dummyExperiment.name, false) } } From 4ff6fa67f8d082e47eba911077fc02186d87e6e6 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 03:02:04 -0300 Subject: [PATCH 08/13] Revert removal of maven central from AutomatticTracks build.gradle repositories --- AutomatticTracks/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/AutomatticTracks/build.gradle b/AutomatticTracks/build.gradle index 19707d1b..420c1d26 100644 --- a/AutomatticTracks/build.gradle +++ b/AutomatticTracks/build.gradle @@ -6,6 +6,7 @@ plugins { repositories { google() + mavenCentral() } dependencies { From 173dd69085a97df859330023dfdee12f821bef0b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 03:06:17 -0300 Subject: [PATCH 09/13] Remove unused files, unnecessary code from :experimentation buildgradle and :sampletracksapp build.gradle --- experimentation/.gitignore | 1 - experimentation/build.gradle | 8 -------- experimentation/consumer-rules.pro | 0 experimentation/proguard-rules.pro | 21 --------------------- sampletracksapp/build.gradle | 3 --- 5 files changed, 33 deletions(-) delete mode 100644 experimentation/.gitignore delete mode 100644 experimentation/consumer-rules.pro delete mode 100644 experimentation/proguard-rules.pro diff --git a/experimentation/.gitignore b/experimentation/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/experimentation/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/experimentation/build.gradle b/experimentation/build.gradle index d4850fbf..38855004 100644 --- a/experimentation/build.gradle +++ b/experimentation/build.gradle @@ -25,14 +25,6 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/experimentation/consumer-rules.pro b/experimentation/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/experimentation/proguard-rules.pro b/experimentation/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/experimentation/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sampletracksapp/build.gradle b/sampletracksapp/build.gradle index be0358b9..859b6f97 100644 --- a/sampletracksapp/build.gradle +++ b/sampletracksapp/build.gradle @@ -5,9 +5,6 @@ plugins { repositories { google() - maven { - url 'https://a8c-libs.s3.amazonaws.com/android' - } } android { From 4cd633b475fd98b8921eac89caaf62b3b4d2dc27 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 03:20:22 -0300 Subject: [PATCH 10/13] Remove example UI test, fix ktlint --- .../ExampleInstrumentedTest.kt | 24 --------- .../android/experimentation/ExPlat.kt | 14 ++--- .../android/experimentation/Experiment.kt | 8 +-- .../example/ExampleExperiment.kt | 5 +- .../android/experimentation/ExPlatTest.kt | 52 +++++++++---------- 5 files changed, 38 insertions(+), 65 deletions(-) delete mode 100644 experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt diff --git a/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt b/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt deleted file mode 100644 index 855f14c9..00000000 --- a/experimentation/src/androidTest/java/com/automattic/android/experimentation/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.automattic.android.experimentation - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.automattic.android.experiment.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt index 0efad766..ee90dd56 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt @@ -14,11 +14,11 @@ import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.util.AppLog.T class ExPlat( - private val experiments: Set, - private val experimentStore: ExperimentStore, - private val appLogWrapper: AppLogWrapper, - private val coroutineScope: CoroutineScope, - private val isDebug: Boolean + private val experiments: Set, + private val experimentStore: ExperimentStore, + private val appLogWrapper: AppLogWrapper, + private val coroutineScope: CoroutineScope, + private val isDebug: Boolean ) { private val platform = Platform.WORDPRESS_ANDROID private val activeVariations = mutableMapOf() @@ -53,13 +53,13 @@ class ExPlat( internal fun getVariation(experimentName: String, shouldRefreshIfStale: Boolean): Variation { if (!experimentNames.contains(experimentName)) { val message = "ExPlat: experiment not found: \"${experimentName}\"! " + - "Make sure to include it in the set provided via constructor." + "Make sure to include it in the set provided via constructor." appLogWrapper.e(T.API, message) if (isDebug) throw IllegalArgumentException(message) else return Control } return activeVariations.getOrPut(experimentName) { getAssignments(if (shouldRefreshIfStale) IF_STALE else NEVER) - .getVariationForExperiment(experimentName) + .getVariationForExperiment(experimentName) } } diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt index 689a9ffe..a82ff728 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/Experiment.kt @@ -3,15 +3,15 @@ package com.automattic.android.experimentation import org.wordpress.android.fluxc.model.experiments.Variation open class Experiment( - val name: String, - private val exPlat: ExPlat + val name: String, + private val exPlat: ExPlat ) { @Suppress("unused") @JvmOverloads fun getVariation(shouldRefreshIfStale: Boolean = false): Variation { return exPlat.getVariation( - experimentName = name, - shouldRefreshIfStale = shouldRefreshIfStale + experimentName = name, + shouldRefreshIfStale = shouldRefreshIfStale ) } } diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt index 41c6cf12..c90b60cf 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt @@ -4,7 +4,4 @@ import com.automattic.android.experimentation.ExPlat import com.automattic.android.experimentation.Experiment @Suppress("unused") -class ExampleExperiment(exPlat: ExPlat) : Experiment( - name = "example_experiment", - exPlat = exPlat -) +class ExampleExperiment(exPlat: ExPlat) : Experiment(name = "example_experiment", exPlat = exPlat) diff --git a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt index 7aa26bc6..b951672e 100644 --- a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt +++ b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt @@ -28,16 +28,16 @@ class ExPlatTest { private val experimentStore: ExperimentStore = mock() private val appLogWrapper: AppLogWrapper = mock() private var exPlat: ExPlat = createExPlat( - isDebug = false, - experiments = emptySet() + isDebug = false, + experiments = emptySet() ) private val dummyExperiment = object : Experiment("dummy", exPlat) {} @Test fun `refreshIfNeeded fetches assignments if cache is null`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) @@ -49,8 +49,8 @@ class ExPlatTest { @Test fun `refreshIfNeeded fetches assignments if cache is stale`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) @@ -71,8 +71,8 @@ class ExPlatTest { @Test fun `forceRefresh fetches assignments if cache is fresh`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) @@ -91,8 +91,8 @@ class ExPlatTest { @Test fun `getVariation fetches assignments if cache is null`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) setupAssignments(cachedAssignments = null, fetchedAssignments = buildAssignments()) @@ -104,8 +104,8 @@ class ExPlatTest { @Test fun `getVariation fetches assignments if cache is stale`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) setupAssignments(cachedAssignments = buildAssignments(isStale = true), fetchedAssignments = buildAssignments()) @@ -164,8 +164,8 @@ class ExPlatTest { @Test fun `forceRefresh fetches assignments if experiments is not empty`() = runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = setOf(dummyExperiment) + isDebug = true, + experiments = setOf(dummyExperiment) ) exPlat.forceRefresh() @@ -201,31 +201,31 @@ class ExPlatTest { fun `getVariation throws IllegalArgumentException if experiment was not found and is debug`() { runBlockingTest { exPlat = createExPlat( - isDebug = true, - experiments = emptySet() + isDebug = true, + experiments = emptySet() ) exPlat.getVariation(dummyExperiment.name, false) } } private fun createExPlat(isDebug: Boolean, experiments: Set): ExPlat = - ExPlat( - experiments = experiments, - experimentStore = experimentStore, - appLogWrapper = appLogWrapper, - coroutineScope = CoroutineScope(Dispatchers.Unconfined), - isDebug = isDebug - ) + ExPlat( + experiments = experiments, + experimentStore = experimentStore, + appLogWrapper = appLogWrapper, + coroutineScope = CoroutineScope(Dispatchers.Unconfined), + isDebug = isDebug + ) private suspend fun setupAssignments(cachedAssignments: Assignments?, fetchedAssignments: Assignments) { whenever(experimentStore.getCachedAssignments()).thenReturn(cachedAssignments) whenever(experimentStore.fetchAssignments(any(), any(), anyOrNull())) - .thenReturn(OnAssignmentsFetched(fetchedAssignments)) + .thenReturn(OnAssignmentsFetched(fetchedAssignments)) } private fun buildAssignments( - isStale: Boolean = false, - variations: Map = emptyMap() + isStale: Boolean = false, + variations: Map = emptyMap() ): Assignments { val now = System.currentTimeMillis() val oneHourAgo = now - ONE_HOUR_IN_SECONDS * 1000 From 258e85e587d38da5083f06907d04ab4c2db6c25b Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 03:27:19 -0300 Subject: [PATCH 11/13] Change targetSdkVersion to match other modules in the repo (30), add lintOptions from :AutomatticTracks to :experimentation build.gradle --- experimentation/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/experimentation/build.gradle b/experimentation/build.gradle index 38855004..bae5f898 100644 --- a/experimentation/build.gradle +++ b/experimentation/build.gradle @@ -20,7 +20,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -30,6 +30,11 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + lintOptions { + baseline file("lint-baseline.xml") + warningsAsErrors true + warning 'GradleDependency' + } } dependencies { From d0dd063cdbf72007b99ff1053520e895d410e70d Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Fri, 4 Mar 2022 14:32:03 -0300 Subject: [PATCH 12/13] Apply PR suggestions: clean unnecessary code in :experimentation build.gradle, inject Platform field in ExPlat, update ExPlat unit tests --- experimentation/build.gradle | 3 --- .../android/experimentation/ExPlat.kt | 2 +- .../example/ExampleExperiment.kt | 7 ----- .../android/experimentation/ExPlatTest.kt | 26 +++++++++++-------- 4 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt diff --git a/experimentation/build.gradle b/experimentation/build.gradle index bae5f898..06ed456b 100644 --- a/experimentation/build.gradle +++ b/experimentation/build.gradle @@ -23,15 +23,12 @@ android { targetSdkVersion 30 versionCode 1 versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { - baseline file("lint-baseline.xml") warningsAsErrors true warning 'GradleDependency' } diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt index ee90dd56..bf5ccaff 100644 --- a/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt +++ b/experimentation/src/main/java/com/automattic/android/experimentation/ExPlat.kt @@ -14,13 +14,13 @@ import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.util.AppLog.T class ExPlat( + private val platform: Platform, private val experiments: Set, private val experimentStore: ExperimentStore, private val appLogWrapper: AppLogWrapper, private val coroutineScope: CoroutineScope, private val isDebug: Boolean ) { - private val platform = Platform.WORDPRESS_ANDROID private val activeVariations = mutableMapOf() private val experimentNames: List by lazy { experiments.map { it.name } } diff --git a/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt b/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt deleted file mode 100644 index c90b60cf..00000000 --- a/experimentation/src/main/java/com/automattic/android/experimentation/example/ExampleExperiment.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.automattic.android.experimentation.example - -import com.automattic.android.experimentation.ExPlat -import com.automattic.android.experimentation.Experiment - -@Suppress("unused") -class ExampleExperiment(exPlat: ExPlat) : Experiment(name = "example_experiment", exPlat = exPlat) diff --git a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt index b951672e..a3bbb2f7 100644 --- a/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt +++ b/experimentation/src/test/java/com/automattic/android/experimentation/ExPlatTest.kt @@ -8,6 +8,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -20,11 +21,13 @@ import org.wordpress.android.fluxc.model.experiments.Variation.Control import org.wordpress.android.fluxc.model.experiments.Variation.Treatment import org.wordpress.android.fluxc.store.ExperimentStore import org.wordpress.android.fluxc.store.ExperimentStore.OnAssignmentsFetched +import org.wordpress.android.fluxc.store.ExperimentStore.Platform import org.wordpress.android.fluxc.utils.AppLogWrapper import java.util.Date @ExperimentalCoroutinesApi class ExPlatTest { + private val platform = Platform.WORDPRESS_ANDROID private val experimentStore: ExperimentStore = mock() private val appLogWrapper: AppLogWrapper = mock() private var exPlat: ExPlat = createExPlat( @@ -43,7 +46,7 @@ class ExPlatTest { exPlat.refreshIfNeeded() - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -56,7 +59,7 @@ class ExPlatTest { exPlat.refreshIfNeeded() - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -65,7 +68,7 @@ class ExPlatTest { exPlat.refreshIfNeeded() - verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, never()).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -78,7 +81,7 @@ class ExPlatTest { exPlat.forceRefresh() - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -98,7 +101,7 @@ class ExPlatTest { exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -111,7 +114,7 @@ class ExPlatTest { exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -120,7 +123,7 @@ class ExPlatTest { exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = true) - verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, never()).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -129,7 +132,7 @@ class ExPlatTest { exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) - verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, never()).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -138,7 +141,7 @@ class ExPlatTest { exPlat.getVariation(dummyExperiment.name, shouldRefreshIfStale = false) - verify(experimentStore, never()).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, never()).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -169,7 +172,7 @@ class ExPlatTest { ) exPlat.forceRefresh() - verify(experimentStore, times(1)).fetchAssignments(any(), any(), anyOrNull()) + verify(experimentStore, times(1)).fetchAssignments(eq(platform), any(), anyOrNull()) } @Test @@ -210,6 +213,7 @@ class ExPlatTest { private fun createExPlat(isDebug: Boolean, experiments: Set): ExPlat = ExPlat( + platform = platform, experiments = experiments, experimentStore = experimentStore, appLogWrapper = appLogWrapper, @@ -219,7 +223,7 @@ class ExPlatTest { private suspend fun setupAssignments(cachedAssignments: Assignments?, fetchedAssignments: Assignments) { whenever(experimentStore.getCachedAssignments()).thenReturn(cachedAssignments) - whenever(experimentStore.fetchAssignments(any(), any(), anyOrNull())) + whenever(experimentStore.fetchAssignments(eq(platform), any(), anyOrNull())) .thenReturn(OnAssignmentsFetched(fetchedAssignments)) } From b09711eb2b9f34f60c59eab3f331ee88d25011b8 Mon Sep 17 00:00:00 2001 From: Renan Lukas <14964993+RenanLukas@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:21:21 -0300 Subject: [PATCH 13/13] Fix PR comment: different compileSdkVersion for :experimentation --- experimentation/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimentation/build.gradle b/experimentation/build.gradle index 06ed456b..54f52c24 100644 --- a/experimentation/build.gradle +++ b/experimentation/build.gradle @@ -16,7 +16,7 @@ repositories { } android { - compileSdkVersion 32 + compileSdkVersion 30 defaultConfig { minSdkVersion 21