diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 5f9be100894f453a47b45502b8a32ce2305f93a7..c303fb301158f82764317f80edc684dc9fc6482f 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -263,6 +263,7 @@ dependencies { testImplementation "io.mockk:mockk:1.10.0" testImplementation "com.squareup.okhttp3:mockwebserver:4.8.0" testImplementation 'org.hamcrest:hamcrest-library:2.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' // Testing - jUnit4 testImplementation 'junit:junit:4.13' diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee6c36d9f96b314781cfc8224b5f1e5f20a1519a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.transaction + +import javax.inject.Inject +import javax.inject.Singleton + +// TODO Remove once we have refactored the transaction and it's no longer a singleton +@Singleton +data class RetrieveDiagnosisInjectionHelper @Inject constructor( + val transactionScope: TransactionCoroutineScope +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt index 10b9903876022ea85f976ecfde588cf53a9ffcc7..a1c4545957be49f1e747dc96ac58d87479d20746 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt @@ -36,6 +36,7 @@ import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.Retriev import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.rollback import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.start import de.rki.coronawarnapp.util.CachedKeyFileHolder +import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.worker.BackgroundWorkHelper import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -118,6 +119,10 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { /** atomic reference for the rollback value for created files during the transaction */ private val exportFilesForRollback = AtomicReference<List<File>>() + private val transactionScope: TransactionCoroutineScope by lazy { + AppInjector.component.transRetrieveKeysInjection.transactionScope + } + suspend fun startWithConstraints() { val currentDate = DateTime(Instant.now(), DateTimeZone.getDefault()) val lastFetch = DateTime( @@ -136,7 +141,7 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { } /** initiates the transaction. This suspend function guarantees a successful transaction once completed. */ - suspend fun start() = lockAndExecuteUnique { + suspend fun start() = lockAndExecute(unique = true, scope = transactionScope) { /** * Handles the case when the ENClient got disabled but the Transaction is still scheduled * in a background job. Also it acts as a failure catch in case the orchestration code did @@ -145,7 +150,7 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { if (!InternalExposureNotificationClient.asyncIsEnabled()) { Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys") executeClose() - return@lockAndExecuteUnique + return@lockAndExecute } /**************************************************** * INIT TRANSACTION diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelInjectionHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5e6689c6319a39c2dd5d9eb59ad0a12c6919d2c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelInjectionHelper.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.transaction + +import javax.inject.Inject +import javax.inject.Singleton + +// TODO Remove once we have refactored the transaction and it's no longer a singleton +@Singleton +data class RiskLevelInjectionHelper @Inject constructor( + val transactionScope: TransactionCoroutineScope +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt index 8bd5312505909fafeaae8902f9763309c2401820..4629e0eb7f3139d8b30b26fc406d71befb4fcfda 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt @@ -37,6 +37,7 @@ import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactio import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.UPDATE_RISK_LEVEL import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours +import de.rki.coronawarnapp.util.di.AppInjector import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @@ -184,8 +185,12 @@ object RiskLevelTransaction : Transaction() { /** atomic reference for the rollback value for date of last risk level calculation */ private val lastCalculatedRiskLevelDate = AtomicReference<Long>() + private val transactionScope: TransactionCoroutineScope by lazy { + AppInjector.component.transRiskLevelInjection.transactionScope + } + /** initiates the transaction. This suspend function guarantees a successful transaction once completed. */ - suspend fun start() = lockAndExecute { + suspend fun start() = lockAndExecute(scope = transactionScope) { /**************************************************** * CHECK [NO_CALCULATION_POSSIBLE_TRACING_OFF] CONDITIONS ****************************************************/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisInjectionHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ddfa7fd1ac4fa01794024f022dc0230c4e68018 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisInjectionHelper.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.transaction + +import javax.inject.Inject +import javax.inject.Singleton + +// TODO Remove once we have refactored the transaction and it's no longer a singleton +@Singleton +data class SubmitDiagnosisInjectionHelper @Inject constructor( + val transactionScope: TransactionCoroutineScope +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt index 326cffe113fbaf6494681cca328f29885de533d7..dddc3798253c03cee64fc09869c52d359c1e5ad0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDia import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDiagnosisKeysTransactionState.STORE_SUCCESS import de.rki.coronawarnapp.util.ProtoFormatConverterExtensions.limitKeyCount import de.rki.coronawarnapp.util.ProtoFormatConverterExtensions.transformKeyHistoryToExternalFormat +import de.rki.coronawarnapp.util.di.AppInjector /** * The SubmitDiagnosisKeysTransaction is used to define an atomic Transaction for Key Reports. Its states allow an @@ -47,16 +48,20 @@ object SubmitDiagnosisKeysTransaction : Transaction() { CLOSE } + private val transactionScope: TransactionCoroutineScope by lazy { + AppInjector.component.transSubmitDiagnosisInjection.transactionScope + } + /** initiates the transaction. This suspend function guarantees a successful transaction once completed. */ suspend fun start( registrationToken: String, keys: List<TemporaryExposureKey> - ) = lockAndExecuteUnique { + ) = lockAndExecute(unique = true, scope = transactionScope) { /**************************************************** * RETRIEVE TEMPORARY EXPOSURE KEY HISTORY ****************************************************/ val temporaryExposureKeyList = executeState(RETRIEVE_TEMPORARY_EXPOSURE_KEY_HISTORY) { - keys.limitKeyCount() + keys.limitKeyCount() .transformKeyHistoryToExternalFormat() } /**************************************************** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt index 54e5d219f90a9d1f59ba21844a72ea651e750e03..a732d93fbffe2a7a41010b207d149501e427f0c1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt @@ -26,6 +26,7 @@ import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.transaction.Transaction.InternalTransactionStates.INIT import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -53,11 +54,11 @@ abstract class Transaction { * Transaction Timeout in Milliseconds, used to cancel any Transactions that run into never ending execution * (e.g. due to a coroutine not being cancelled properly or an exception leading to unchecked behavior) */ - private val TRANSACTION_TIMEOUT_MS = - TimeVariables.getTransactionTimeout() + private val TRANSACTION_TIMEOUT_MS: Long + get() = TimeVariables.getTransactionTimeout() } - @Suppress("VariableNaming") // Done as the Convention is TAG for every class + @Suppress("VariableNaming", "PropertyName") // Done as the Convention is TAG for every class abstract val TAG: String? /** @@ -173,26 +174,6 @@ abstract class Transaction { ): T = executeState(Dispatchers.Default, state, block) - /** - * Executes the transaction as Unique. This results in the next execution being omitted in case of a race towards - * the lock. Please see [lockAndExecute] for more details - * - * @param T Optional Return Type in case the Transaction should return a value from the coroutine. - * @param block the suspending function that should be used to execute the transaction. - */ - protected suspend fun <T> lockAndExecuteUnique(block: suspend CoroutineScope.() -> T) = - lockAndExecute(true, block) - - /** - * Executes the transaction as non-unique. This results in the next execution being queued in case of a race towards - * the lock. Please see [lockAndExecute] for more details - * - * @param T Optional Return Type in case the Transaction should return a value from the coroutine. - * @param block the suspending function that should be used to execute the transaction. - */ - protected suspend fun <T> lockAndExecute(block: suspend CoroutineScope.() -> T) = - lockAndExecute(false, block) - /** * Attempts to go into the internal lock context (mutual exclusion coroutine) and executes the given suspending * function. Standard Logging is executed to inform about the transaction status. @@ -206,37 +187,52 @@ abstract class Transaction { * * In an error scenario, during the handling of the transaction error, a rollback will be executed on best-effort basis. * - * @param T Optional Return Type in case the Transaction should return a value from the coroutine. + * @param unique Executes the transaction as Unique. This results in the next execution being omitted in case of a race towards the lock. * @param block the suspending function that should be used to execute the transaction. * @throws TransactionException the exception that wraps around any error that occurs inside the lock. * * @see executeState * @see executedStatesStack */ - private suspend fun <T> lockAndExecute(unique: Boolean, block: suspend CoroutineScope.() -> T) { + suspend fun lockAndExecute( + unique: Boolean = false, + scope: CoroutineScope, + block: suspend CoroutineScope.() -> Unit + ) { + if (unique && internalMutualExclusionLock.isLocked) { - val runningString = "TRANSACTION WITH ID $transactionId ALREADY RUNNING " + - "($currentTransactionState) AS UNIQUE, SKIPPING EXECUTION." - Timber.tag(TAG).w(runningString) + Timber.tag(TAG).w( + "TRANSACTION WITH ID %s ALREADY RUNNING (%s) AS UNIQUE, SKIPPING EXECUTION.", + transactionId, currentTransactionState + ) return } - try { - return internalMutualExclusionLock.withLock { + + val deferred = scope.async { + internalMutualExclusionLock.withLock { executeState(INIT) { transactionId.set(UUID.randomUUID()) } - measureTimeMillis { + + val duration = measureTimeMillis { withTimeout(TRANSACTION_TIMEOUT_MS) { block.invoke(this) } - }.also { - val completedString = - "TRANSACTION $transactionId COMPLETED (${System.currentTimeMillis()}) " + - "in $it ms, STATES EXECUTED: ${getExecutedStates()}" - Timber.tag(TAG).i(completedString) } + + Timber.tag(TAG).i( + "TRANSACTION %s COMPLETED (%d) in %d ms, STATES EXECUTED: %s", + transactionId, System.currentTimeMillis(), duration, getExecutedStates() + ) + resetExecutedStateStack() } - } catch (e: Exception) { - handleTransactionError(e) + } + + withContext(scope.coroutineContext) { + try { + deferred.await() + } catch (e: Exception) { + handleTransactionError(e) + } } } @@ -253,14 +249,18 @@ abstract class Transaction { * @param error the error that lead to an error case in the transaction that cannot be handled inside the * transaction but has to be caught from the exception caller */ - protected open suspend fun handleTransactionError(error: Throwable?): Nothing { - rollback() - resetExecutedStateStack() - throw TransactionException( + protected open suspend fun handleTransactionError(error: Throwable): Nothing { + val wrap = TransactionException( transactionId.get(), currentTransactionState.toString(), error ) + Timber.tag(TAG).e(wrap) + + rollback() + resetExecutedStateStack() + + throw wrap } /** @@ -285,10 +285,12 @@ abstract class Transaction { * @param error the error that lead to an error case in the rollback */ protected open fun handleRollbackError(error: Throwable?): Nothing { - throw RollbackException( + val wrap = RollbackException( transactionId.get(), currentTransactionState.toString(), error ) + Timber.tag(TAG).e(wrap) + throw wrap } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionCoroutineScope.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionCoroutineScope.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e27712b8af6d356585d87e67c6576dec91421ff --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionCoroutineScope.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.transaction + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext + +@Singleton +class TransactionCoroutineScope @Inject constructor() : CoroutineScope { + override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Default +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt index dbebf2871e85a8b2bcdd35ae40c23c0d4d906c2a..daa9dc01afa8314cedb3829fa1b60a27dfae31f1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt @@ -3,9 +3,10 @@ package de.rki.coronawarnapp.util.di import de.rki.coronawarnapp.CoronaWarnApplication object AppInjector { + lateinit var component: ApplicationComponent + fun init(app: CoronaWarnApplication) { - DaggerApplicationComponent.factory() - .create(app) - .inject(app) + component = DaggerApplicationComponent.factory().create(app) + component.inject(app) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt index 914a6a9f5a98b61f67cbf3d680e80de613d04c59..691b28bd77cf966da95fd440e6085b5713c475ac 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.util.di +import dagger.BindsInstance import dagger.Component import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule @@ -7,6 +8,9 @@ import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.receiver.ReceiverBinder import de.rki.coronawarnapp.risk.RiskModule import de.rki.coronawarnapp.service.ServiceBinder +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisInjectionHelper +import de.rki.coronawarnapp.transaction.RiskLevelInjectionHelper +import de.rki.coronawarnapp.transaction.SubmitDiagnosisInjectionHelper import de.rki.coronawarnapp.ui.ActivityBinder import de.rki.coronawarnapp.util.device.DeviceModule import javax.inject.Singleton @@ -24,6 +28,13 @@ import javax.inject.Singleton ] ) interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { + + val transRetrieveKeysInjection: RetrieveDiagnosisInjectionHelper + val transRiskLevelInjection: RiskLevelInjectionHelper + val transSubmitDiagnosisInjection: SubmitDiagnosisInjectionHelper + @Component.Factory - interface Factory : AndroidInjector.Factory<CoronaWarnApplication> + interface Factory { + fun create(@BindsInstance app: CoronaWarnApplication): ApplicationComponent + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt index e8c59395e8583acf1d6313379823f3b75d1edb94..893a93928d7afaa98a80ec3cc57645c2ec73f3d1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt @@ -4,6 +4,8 @@ import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.di.ApplicationComponent import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerifyOrder @@ -28,6 +30,14 @@ class RetrieveDiagnosisKeysTransactionTest { @Before fun setUp() { + mockkObject(AppInjector) + val appComponent = mockk<ApplicationComponent>().apply { + every { transRetrieveKeysInjection } returns RetrieveDiagnosisInjectionHelper( + TransactionCoroutineScope() + ) + } + every { AppInjector.component } returns appComponent + mockkObject(InternalExposureNotificationClient) mockkObject(ApplicationConfigurationService) mockkObject(RetrieveDiagnosisKeysTransaction) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt index 212a34ee6e7ab83ef266c74882eeef5603e14569..3634926310b6126a27887872df35457d73eebb72 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt @@ -23,6 +23,8 @@ import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.util.ConnectivityHelper +import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.di.ApplicationComponent import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery @@ -30,6 +32,7 @@ import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkAll import kotlinx.coroutines.runBlocking @@ -52,6 +55,14 @@ class RiskLevelTransactionTest { fun setUp() { MockKAnnotations.init(this) + mockkObject(AppInjector) + val appComponent = mockk<ApplicationComponent>().apply { + every { transRiskLevelInjection } returns RiskLevelInjectionHelper( + TransactionCoroutineScope() + ) + } + every { AppInjector.component } returns appComponent + mockkObject(InternalExposureNotificationClient) mockkObject(ApplicationConfigurationService) mockkObject(LocalData) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransactionTest.kt index c43d6b9ebd0d1fc9e9c3ef6d4cb9925941e5317f..25d61722c61202975cab25cf1bcdea121ffe6fd0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransactionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransactionTest.kt @@ -7,6 +7,8 @@ import de.rki.coronawarnapp.http.playbook.BackgroundNoise import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import io.mockk.MockKAnnotations import io.mockk.Runs @@ -15,6 +17,7 @@ import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot import io.mockk.unmockkAll @@ -40,6 +43,14 @@ class SubmitDiagnosisKeysTransactionTest { fun setUp() { MockKAnnotations.init(this) + mockkObject(AppInjector) + val appComponent = mockk<ApplicationComponent>().apply { + every { transSubmitDiagnosisInjection } returns SubmitDiagnosisInjectionHelper( + TransactionCoroutineScope() + ) + } + every { AppInjector.component } returns appComponent + mockkObject(WebRequestBuilder.Companion) every { WebRequestBuilder.getInstance() } returns webRequestBuilder diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ba62a995d754feedd5966522f036871c2f0da84 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt @@ -0,0 +1,113 @@ +package de.rki.coronawarnapp.transaction + +import de.rki.coronawarnapp.exception.RollbackException +import de.rki.coronawarnapp.exception.TransactionException +import de.rki.coronawarnapp.risk.TimeVariables +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.should +import io.kotest.matchers.types.beInstanceOf +import io.mockk.clearAllMocks +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.io.IOException + +@ExperimentalCoroutinesApi +class TransactionTest : BaseTest() { + + @BeforeEach + fun setup() { + mockkObject(TimeVariables) + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + @Suppress("UNREACHABLE_CODE") + private class TestTransaction( + val errorOnRollBack: Exception? = null + ) : Transaction() { + override val TAG: String = "TestTag" + + public override suspend fun rollback() { + errorOnRollBack?.let { handleRollbackError(it) } + super.rollback() + } + + public override suspend fun handleTransactionError(error: Throwable): Nothing { + return super.handleTransactionError(error) + } + + public override fun handleRollbackError(error: Throwable?): Nothing { + return super.handleRollbackError(error) + } + } + + @Test + fun `transaction error handler is called`() { + val testScope = TestCoroutineScope() + val testTransaction = spyk(TestTransaction()) + shouldThrow<TransactionException> { + runBlocking { + testTransaction.lockAndExecute(scope = testScope) { + throw IOException() + } + } + } + + coVerify { testTransaction.handleTransactionError(any()) } + coVerify { testTransaction.rollback() } + } + + @Test + fun `rollback error handler is called`() { + val testScope = TestCoroutineScope() + val testTransaction = spyk( + TestTransaction( + errorOnRollBack = IllegalAccessException() + ) + ) + shouldThrow<RollbackException> { + runBlocking { + testTransaction.lockAndExecute(scope = testScope) { + throw IOException() + } + } + } + + coVerify { testTransaction.handleTransactionError(ofType<IOException>()) } + coVerify { testTransaction.rollback() } + coVerify { testTransaction.handleRollbackError(ofType<IllegalAccessException>()) } + } + + @Test + fun `transactions can timeout`() { + /** + * TODO use runBlockingTest & advanceTime, which currently does not work + * https://github.com/Kotlin/kotlinx.coroutines/issues/1204 + */ + every { TimeVariables.getTransactionTimeout() } returns 0L + + val testTransaction = TestTransaction() + val exception = shouldThrow<TransactionException> { + runBlocking { + testTransaction.lockAndExecute(scope = this) { + delay(TimeVariables.getTransactionTimeout()) + } + } + } + exception.cause should beInstanceOf<TimeoutCancellationException>() + } +}