summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-12-09 10:07:54 +0530
committerGitHub <noreply@github.com>2021-12-09 04:37:54 +0000
commit8db0b67ce9ba4a5e56c04c1bea3a738eacb176cf (patch)
tree46700b7a3da32065e073bffbf22765c7e2f30d32
parent933558caf8266677dc26497d7c7b930254f4fb07 (diff)
Refactor coroutine testing setup (#1583)
* coroutine-utils: init * coroutine-utils-testing: init * format-common: switch over to using DispatcherProvider * Convert Binds method to an extension function * Add Dispatcher module
-rw-r--r--app/build.gradle.kts1
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/injection/coroutines/DispatcherModule.kt19
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/injection/totp/TotpModule.kt2
-rw-r--r--coroutine-utils-testing/api/coroutine-utils-testing.api8
-rw-r--r--coroutine-utils-testing/build.gradle.kts14
-rw-r--r--coroutine-utils-testing/src/main/kotlin/dev/msfjarvis/aps/test/CoroutineTestRule.kt45
-rw-r--r--coroutine-utils/api/coroutine-utils.api22
-rw-r--r--coroutine-utils/build.gradle.kts13
-rw-r--r--coroutine-utils/src/main/kotlin/dev/msfjarvis/aps/util/coroutines/DispatcherProvider.kt22
-rw-r--r--format-common/api/format-common.api2
-rw-r--r--format-common/build.gradle.kts2
-rw-r--r--format-common/src/main/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntry.kt5
-rw-r--r--format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt25
-rw-r--r--settings.gradle.kts6
14 files changed, 173 insertions, 13 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 855f1189..d0a0dab4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -74,6 +74,7 @@ dependencies {
implementation(libs.androidx.annotation)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
implementation(projects.autofillParser)
+ implementation(projects.coroutineUtils)
implementation(projects.cryptoPgpainless)
implementation(projects.formatCommon)
implementation(projects.openpgpKtx)
diff --git a/app/src/main/java/dev/msfjarvis/aps/injection/coroutines/DispatcherModule.kt b/app/src/main/java/dev/msfjarvis/aps/injection/coroutines/DispatcherModule.kt
new file mode 100644
index 00000000..bf84fc27
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/injection/coroutines/DispatcherModule.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.injection.coroutines
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import dev.msfjarvis.aps.util.coroutines.DefaultDispatcherProvider
+import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface DispatcherModule {
+ @Binds fun DefaultDispatcherProvider.bind(): DispatcherProvider
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/injection/totp/TotpModule.kt b/app/src/main/java/dev/msfjarvis/aps/injection/totp/TotpModule.kt
index e02a3b86..859559cd 100644
--- a/app/src/main/java/dev/msfjarvis/aps/injection/totp/TotpModule.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/injection/totp/TotpModule.kt
@@ -15,5 +15,5 @@ import dev.msfjarvis.aps.util.totp.UriTotpFinder
@Module
@InstallIn(ActivityComponent::class)
interface TotpModule {
- @Binds abstract fun bindTotpFinder(totpFinder: UriTotpFinder): TotpFinder
+ @Binds fun UriTotpFinder.bind(): TotpFinder
}
diff --git a/coroutine-utils-testing/api/coroutine-utils-testing.api b/coroutine-utils-testing/api/coroutine-utils-testing.api
new file mode 100644
index 00000000..a90e209f
--- /dev/null
+++ b/coroutine-utils-testing/api/coroutine-utils-testing.api
@@ -0,0 +1,8 @@
+public final class dev/msfjarvis/aps/test/CoroutineTestRule : org/junit/rules/TestWatcher {
+ public fun <init> ()V
+ public fun <init> (Lkotlinx/coroutines/test/TestDispatcher;)V
+ public synthetic fun <init> (Lkotlinx/coroutines/test/TestDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getTestDispatcher ()Lkotlinx/coroutines/test/TestDispatcher;
+ public final fun getTestDispatcherProvider ()Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;
+}
+
diff --git a/coroutine-utils-testing/build.gradle.kts b/coroutine-utils-testing/build.gradle.kts
new file mode 100644
index 00000000..96d87dc1
--- /dev/null
+++ b/coroutine-utils-testing/build.gradle.kts
@@ -0,0 +1,14 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+plugins {
+ kotlin("jvm")
+ id("com.github.android-password-store.kotlin-library")
+}
+
+dependencies {
+ implementation(projects.coroutineUtils)
+ implementation(libs.testing.junit)
+ implementation(libs.kotlin.coroutines.test)
+}
diff --git a/coroutine-utils-testing/src/main/kotlin/dev/msfjarvis/aps/test/CoroutineTestRule.kt b/coroutine-utils-testing/src/main/kotlin/dev/msfjarvis/aps/test/CoroutineTestRule.kt
new file mode 100644
index 00000000..fa4a2d41
--- /dev/null
+++ b/coroutine-utils-testing/src/main/kotlin/dev/msfjarvis/aps/test/CoroutineTestRule.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.test
+
+import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * JUnit [TestWatcher] to correctly handle setting and resetting a given [testDispatcher] for tests.
+ */
+@ExperimentalCoroutinesApi
+public class CoroutineTestRule(
+ public val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler()),
+) : TestWatcher() {
+
+ public val testDispatcherProvider: DispatcherProvider =
+ object : DispatcherProvider {
+ override fun default(): CoroutineDispatcher = testDispatcher
+ override fun io(): CoroutineDispatcher = testDispatcher
+ override fun main(): CoroutineDispatcher = testDispatcher
+ override fun unconfined(): CoroutineDispatcher = testDispatcher
+ }
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ }
+}
diff --git a/coroutine-utils/api/coroutine-utils.api b/coroutine-utils/api/coroutine-utils.api
new file mode 100644
index 00000000..000dde65
--- /dev/null
+++ b/coroutine-utils/api/coroutine-utils.api
@@ -0,0 +1,22 @@
+public final class dev/msfjarvis/aps/util/coroutines/DefaultDispatcherProvider : dev/msfjarvis/aps/util/coroutines/DispatcherProvider {
+ public fun <init> ()V
+ public fun default ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public fun io ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public fun main ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public fun unconfined ()Lkotlinx/coroutines/CoroutineDispatcher;
+}
+
+public abstract interface class dev/msfjarvis/aps/util/coroutines/DispatcherProvider {
+ public abstract fun default ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public abstract fun io ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public abstract fun main ()Lkotlinx/coroutines/CoroutineDispatcher;
+ public abstract fun unconfined ()Lkotlinx/coroutines/CoroutineDispatcher;
+}
+
+public final class dev/msfjarvis/aps/util/coroutines/DispatcherProvider$DefaultImpls {
+ public static fun default (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
+ public static fun io (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
+ public static fun main (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
+ public static fun unconfined (Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;)Lkotlinx/coroutines/CoroutineDispatcher;
+}
+
diff --git a/coroutine-utils/build.gradle.kts b/coroutine-utils/build.gradle.kts
new file mode 100644
index 00000000..2a20df08
--- /dev/null
+++ b/coroutine-utils/build.gradle.kts
@@ -0,0 +1,13 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+plugins {
+ kotlin("jvm")
+ id("com.github.android-password-store.kotlin-library")
+}
+
+dependencies {
+ implementation(libs.kotlin.coroutines.core)
+ implementation(libs.dagger.hilt.core)
+}
diff --git a/coroutine-utils/src/main/kotlin/dev/msfjarvis/aps/util/coroutines/DispatcherProvider.kt b/coroutine-utils/src/main/kotlin/dev/msfjarvis/aps/util/coroutines/DispatcherProvider.kt
new file mode 100644
index 00000000..a7c4530d
--- /dev/null
+++ b/coroutine-utils/src/main/kotlin/dev/msfjarvis/aps/util/coroutines/DispatcherProvider.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.coroutines
+
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+
+/** Interface to allow abstracting individual [CoroutineDispatcher]s out of class dependencies. */
+public interface DispatcherProvider {
+
+ public fun main(): CoroutineDispatcher = Dispatchers.Main
+ public fun default(): CoroutineDispatcher = Dispatchers.Default
+ public fun io(): CoroutineDispatcher = Dispatchers.IO
+ public fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
+}
+
+/** Concrete type for [DispatcherProvider] with all the defaults from the class. */
+public class DefaultDispatcherProvider @Inject constructor() : DispatcherProvider
diff --git a/format-common/api/format-common.api b/format-common/api/format-common.api
index d0caaf78..629644e3 100644
--- a/format-common/api/format-common.api
+++ b/format-common/api/format-common.api
@@ -1,5 +1,5 @@
public final class dev/msfjarvis/aps/data/passfile/PasswordEntry {
- public fun <init> (Ldev/msfjarvis/aps/util/time/UserClock;Ldev/msfjarvis/aps/util/totp/TotpFinder;Lkotlinx/coroutines/CoroutineScope;[B)V
+ public fun <init> (Ldev/msfjarvis/aps/util/time/UserClock;Ldev/msfjarvis/aps/util/totp/TotpFinder;Ldev/msfjarvis/aps/util/coroutines/DispatcherProvider;Lkotlinx/coroutines/CoroutineScope;[B)V
public final fun getExtraContent ()Ljava/util/Map;
public final fun getExtraContentString ()Ljava/lang/String;
public final fun getExtraContentWithoutAuthData ()Ljava/lang/String;
diff --git a/format-common/build.gradle.kts b/format-common/build.gradle.kts
index e5400bcc..3b940573 100644
--- a/format-common/build.gradle.kts
+++ b/format-common/build.gradle.kts
@@ -9,11 +9,13 @@ plugins {
}
dependencies {
+ implementation(projects.coroutineUtils)
implementation(libs.androidx.annotation)
implementation(libs.dagger.hilt.core)
implementation(libs.thirdparty.commons.codec)
implementation(libs.thirdparty.kotlinResult)
implementation(libs.kotlin.coroutines.core)
+ testImplementation(projects.coroutineUtilsTesting)
testImplementation(libs.bundles.testDependencies)
testImplementation(libs.kotlin.coroutines.test)
}
diff --git a/format-common/src/main/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntry.kt b/format-common/src/main/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntry.kt
index 408069a2..81398b45 100644
--- a/format-common/src/main/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntry.kt
+++ b/format-common/src/main/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntry.kt
@@ -9,6 +9,7 @@ import com.github.michaelbull.result.mapBoth
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
import dev.msfjarvis.aps.util.time.UserClock
import dev.msfjarvis.aps.util.totp.Otp
import dev.msfjarvis.aps.util.totp.TotpFinder
@@ -32,6 +33,8 @@ constructor(
clock: UserClock,
/** [TotpFinder] implementation to extract data from a TOTP URI */
totpFinder: TotpFinder,
+ /** Instance of [DispatcherProvider] to select an IO dispatcher for emitting TOTP values. */
+ dispatcherProvider: DispatcherProvider,
/**
* A cancellable [CoroutineScope] inside which we constantly emit new TOTP values as time elapses
*/
@@ -81,7 +84,7 @@ constructor(
username = findUsername()
totpSecret = totpFinder.findSecret(content)
if (totpSecret != null) {
- scope.launch {
+ scope.launch(dispatcherProvider.io()) {
val digits = totpFinder.findDigits(content)
val totpPeriod = totpFinder.findPeriod(content)
val totpAlgorithm = totpFinder.findAlgorithm(content)
diff --git a/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt b/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt
index 2923946e..32066cc3 100644
--- a/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt
+++ b/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt
@@ -5,6 +5,7 @@
package dev.msfjarvis.aps.data.passfile
+import dev.msfjarvis.aps.test.CoroutineTestRule
import dev.msfjarvis.aps.util.time.TestUserClock
import dev.msfjarvis.aps.util.totp.TotpFinder
import java.util.Locale
@@ -16,15 +17,22 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
+import org.junit.Rule
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
class PasswordEntryTest {
+ @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
private fun makeEntry(content: String) =
- PasswordEntry(fakeClock, testFinder, scope, content.encodeToByteArray())
+ PasswordEntry(
+ fakeClock,
+ testFinder,
+ coroutineTestRule.testDispatcherProvider,
+ TestScope(coroutineTestRule.testDispatcher),
+ content.encodeToByteArray(),
+ )
@Test
fun testGetPassword() {
@@ -125,19 +133,20 @@ class PasswordEntryTest {
@Test
@Ignore("Timing with runTest seems hard to implement right now")
- fun testGeneratesOtpFromTotpUri() =
- scope.runTest {
+ fun testGeneratesOtpFromTotpUri() {
+ runTest {
val entry = makeEntry("secret\nextra\n$TOTP_URI")
assertTrue(entry.hasTotp())
val code = entry.totp.value
assertNotNull(code) { "Generated OTP cannot be null" }
assertEquals("818800", code)
}
+ }
@Test
@Ignore("Timing with runTest seems hard to implement right now")
- fun testGeneratesOtpWithOnlyUriInFile() =
- scope.runTest {
+ fun testGeneratesOtpWithOnlyUriInFile() {
+ runTest {
val entry = makeEntry(TOTP_URI)
assertNull(entry.password)
assertTrue(entry.hasTotp())
@@ -145,6 +154,7 @@ class PasswordEntryTest {
assertNotNull(code) { "Generated OTP cannot be null" }
assertEquals("818800", code)
}
+ }
@Test
fun testOnlyLooksForUriInFirstLine() {
@@ -171,9 +181,6 @@ class PasswordEntryTest {
const val TOTP_URI =
"otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
- val dispatcher = StandardTestDispatcher()
- val scope = TestScope(dispatcher)
-
val fakeClock = TestUserClock()
// This implementation is hardcoded for the URI above.
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6c600dc7..9ea2cd3b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -42,6 +42,10 @@ include("app")
include("autofill-parser")
+include("coroutine-utils")
+
+include("coroutine-utils-testing")
+
include("crypto-common")
include("crypto-pgpainless")
@@ -50,4 +54,4 @@ include("format-common")
include("openpgp-ktx")
-include(":dependency-sync")
+include("dependency-sync")