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
This commit is contained in:
Harsh Shandilya 2021-12-09 10:07:54 +05:30 committed by GitHub
parent 933558caf8
commit 8db0b67ce9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 173 additions and 13 deletions

View file

@ -74,6 +74,7 @@ dependencies {
implementation(libs.androidx.annotation) implementation(libs.androidx.annotation)
coreLibraryDesugaring(libs.android.desugarJdkLibs) coreLibraryDesugaring(libs.android.desugarJdkLibs)
implementation(projects.autofillParser) implementation(projects.autofillParser)
implementation(projects.coroutineUtils)
implementation(projects.cryptoPgpainless) implementation(projects.cryptoPgpainless)
implementation(projects.formatCommon) implementation(projects.formatCommon)
implementation(projects.openpgpKtx) implementation(projects.openpgpKtx)

View file

@ -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
}

View file

@ -15,5 +15,5 @@ import dev.msfjarvis.aps.util.totp.UriTotpFinder
@Module @Module
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
interface TotpModule { interface TotpModule {
@Binds abstract fun bindTotpFinder(totpFinder: UriTotpFinder): TotpFinder @Binds fun UriTotpFinder.bind(): TotpFinder
} }

View file

@ -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;
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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;
}

View file

@ -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)
}

View file

@ -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

View file

@ -1,5 +1,5 @@
public final class dev/msfjarvis/aps/data/passfile/PasswordEntry { 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 getExtraContent ()Ljava/util/Map;
public final fun getExtraContentString ()Ljava/lang/String; public final fun getExtraContentString ()Ljava/lang/String;
public final fun getExtraContentWithoutAuthData ()Ljava/lang/String; public final fun getExtraContentWithoutAuthData ()Ljava/lang/String;

View file

@ -9,11 +9,13 @@ plugins {
} }
dependencies { dependencies {
implementation(projects.coroutineUtils)
implementation(libs.androidx.annotation) implementation(libs.androidx.annotation)
implementation(libs.dagger.hilt.core) implementation(libs.dagger.hilt.core)
implementation(libs.thirdparty.commons.codec) implementation(libs.thirdparty.commons.codec)
implementation(libs.thirdparty.kotlinResult) implementation(libs.thirdparty.kotlinResult)
implementation(libs.kotlin.coroutines.core) implementation(libs.kotlin.coroutines.core)
testImplementation(projects.coroutineUtilsTesting)
testImplementation(libs.bundles.testDependencies) testImplementation(libs.bundles.testDependencies)
testImplementation(libs.kotlin.coroutines.test) testImplementation(libs.kotlin.coroutines.test)
} }

View file

@ -9,6 +9,7 @@ import com.github.michaelbull.result.mapBoth
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dev.msfjarvis.aps.util.coroutines.DispatcherProvider
import dev.msfjarvis.aps.util.time.UserClock import dev.msfjarvis.aps.util.time.UserClock
import dev.msfjarvis.aps.util.totp.Otp import dev.msfjarvis.aps.util.totp.Otp
import dev.msfjarvis.aps.util.totp.TotpFinder import dev.msfjarvis.aps.util.totp.TotpFinder
@ -32,6 +33,8 @@ constructor(
clock: UserClock, clock: UserClock,
/** [TotpFinder] implementation to extract data from a TOTP URI */ /** [TotpFinder] implementation to extract data from a TOTP URI */
totpFinder: TotpFinder, 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 * A cancellable [CoroutineScope] inside which we constantly emit new TOTP values as time elapses
*/ */
@ -81,7 +84,7 @@ constructor(
username = findUsername() username = findUsername()
totpSecret = totpFinder.findSecret(content) totpSecret = totpFinder.findSecret(content)
if (totpSecret != null) { if (totpSecret != null) {
scope.launch { scope.launch(dispatcherProvider.io()) {
val digits = totpFinder.findDigits(content) val digits = totpFinder.findDigits(content)
val totpPeriod = totpFinder.findPeriod(content) val totpPeriod = totpFinder.findPeriod(content)
val totpAlgorithm = totpFinder.findAlgorithm(content) val totpAlgorithm = totpFinder.findAlgorithm(content)

View file

@ -5,6 +5,7 @@
package dev.msfjarvis.aps.data.passfile package dev.msfjarvis.aps.data.passfile
import dev.msfjarvis.aps.test.CoroutineTestRule
import dev.msfjarvis.aps.util.time.TestUserClock import dev.msfjarvis.aps.util.time.TestUserClock
import dev.msfjarvis.aps.util.totp.TotpFinder import dev.msfjarvis.aps.util.totp.TotpFinder
import java.util.Locale import java.util.Locale
@ -16,15 +17,22 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
class PasswordEntryTest { class PasswordEntryTest {
@get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
private fun makeEntry(content: String) = private fun makeEntry(content: String) =
PasswordEntry(fakeClock, testFinder, scope, content.encodeToByteArray()) PasswordEntry(
fakeClock,
testFinder,
coroutineTestRule.testDispatcherProvider,
TestScope(coroutineTestRule.testDispatcher),
content.encodeToByteArray(),
)
@Test @Test
fun testGetPassword() { fun testGetPassword() {
@ -125,19 +133,20 @@ class PasswordEntryTest {
@Test @Test
@Ignore("Timing with runTest seems hard to implement right now") @Ignore("Timing with runTest seems hard to implement right now")
fun testGeneratesOtpFromTotpUri() = fun testGeneratesOtpFromTotpUri() {
scope.runTest { runTest {
val entry = makeEntry("secret\nextra\n$TOTP_URI") val entry = makeEntry("secret\nextra\n$TOTP_URI")
assertTrue(entry.hasTotp()) assertTrue(entry.hasTotp())
val code = entry.totp.value val code = entry.totp.value
assertNotNull(code) { "Generated OTP cannot be null" } assertNotNull(code) { "Generated OTP cannot be null" }
assertEquals("818800", code) assertEquals("818800", code)
} }
}
@Test @Test
@Ignore("Timing with runTest seems hard to implement right now") @Ignore("Timing with runTest seems hard to implement right now")
fun testGeneratesOtpWithOnlyUriInFile() = fun testGeneratesOtpWithOnlyUriInFile() {
scope.runTest { runTest {
val entry = makeEntry(TOTP_URI) val entry = makeEntry(TOTP_URI)
assertNull(entry.password) assertNull(entry.password)
assertTrue(entry.hasTotp()) assertTrue(entry.hasTotp())
@ -145,6 +154,7 @@ class PasswordEntryTest {
assertNotNull(code) { "Generated OTP cannot be null" } assertNotNull(code) { "Generated OTP cannot be null" }
assertEquals("818800", code) assertEquals("818800", code)
} }
}
@Test @Test
fun testOnlyLooksForUriInFirstLine() { fun testOnlyLooksForUriInFirstLine() {
@ -171,9 +181,6 @@ class PasswordEntryTest {
const val TOTP_URI = const val TOTP_URI =
"otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30" "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() val fakeClock = TestUserClock()
// This implementation is hardcoded for the URI above. // This implementation is hardcoded for the URI above.

View file

@ -42,6 +42,10 @@ include("app")
include("autofill-parser") include("autofill-parser")
include("coroutine-utils")
include("coroutine-utils-testing")
include("crypto-common") include("crypto-common")
include("crypto-pgpainless") include("crypto-pgpainless")
@ -50,4 +54,4 @@ include("format-common")
include("openpgp-ktx") include("openpgp-ktx")
include(":dependency-sync") include("dependency-sync")