diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 0ab2b436cb66ae70971a996d43e7695c6eb9704a..014e3d72bfe3bb566dd2dfdab77b99e76068c387 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.CoroutineScope import timber.log.Timber import javax.inject.Inject @@ -16,11 +17,13 @@ class DefaultRiskLevelStorage @Inject constructor( presenceTracingRiskRepository: PresenceTracingRiskRepository, @AppScope scope: CoroutineScope, riskCombinator: RiskCombinator, + timeStamper: TimeStamper, ) : BaseRiskLevelStorage( riskResultDatabaseFactory, presenceTracingRiskRepository, scope, - riskCombinator + riskCombinator, + timeStamper, ) { // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 23a0450fef17a9260ddcd2b06328d3501d15b7c8..545f04c1e9d90ab98ae70084ab92fb845e8647de 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstances +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.firstOrNull @@ -20,11 +21,13 @@ class DefaultRiskLevelStorage @Inject constructor( presenceTracingRiskRepository: PresenceTracingRiskRepository, @AppScope val scope: CoroutineScope, riskCombinator: RiskCombinator, + timeStamper: TimeStamper, ) : BaseRiskLevelStorage( riskResultDatabaseFactory, presenceTracingRiskRepository, scope, riskCombinator, + timeStamper, ) { // 14 days, 6 times per day diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt index cd5d3699d94a1b0e95360c62733c4d504f17edd4..e329e38b84e00c40302926dd85da8186677d2318 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.risk +import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.Instant @@ -13,7 +15,8 @@ data class CombinedEwPtDayRisk( data class CombinedEwPtRiskLevelResult( private val ptRiskLevelResult: PtRiskLevelResult, - private val ewRiskLevelResult: EwRiskLevelResult + private val ewRiskLevelResult: EwRiskLevelResult, + private val exposureWindowDayRisks: List<ExposureWindowDayRisk>? = null ) { val riskState: RiskState by lazy { @@ -31,12 +34,12 @@ data class CombinedEwPtRiskLevelResult( val daysWithEncounters: Int by lazy { when (riskState) { RiskState.INCREASED_RISK -> { - ewRiskLevelResult.daysWithHighRisk + ewDaysWithHighRisk .plus(ptRiskLevelResult.daysWithHighRisk) .distinct().count() } RiskState.LOW_RISK -> { - ewRiskLevelResult.daysWithLowRisk + ewDaysWithLowRisk .plus(ptRiskLevelResult.daysWithLowRisk) .distinct().count() } @@ -66,6 +69,18 @@ data class CombinedEwPtRiskLevelResult( val matchedRiskCount: Int by lazy { ewRiskLevelResult.matchedKeyCount + ptRiskLevelResult.checkInOverlapCount } + + @VisibleForTesting + internal val ewDaysWithHighRisk: List<LocalDate> + get() = exposureWindowDayRisks?.filter { + it.riskLevel.mapToRiskState() == RiskState.INCREASED_RISK + }?.map { it.localDateUtc } ?: emptyList() + + @VisibleForTesting + internal val ewDaysWithLowRisk: List<LocalDate> + get() = exposureWindowDayRisks?.filter { + it.riskLevel.mapToRiskState() == RiskState.LOW_RISK + }?.map { it.localDateUtc } ?: emptyList() } data class LastCombinedRiskResults( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt index daa440e5a5d375f5c0b1ded9a8885c07f8a74aee..397f47d17acf7cbd541445b8244890c3a601e03d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import org.joda.time.Instant -import org.joda.time.LocalDate interface EwRiskLevelResult { val calculatedAt: Instant @@ -43,16 +42,6 @@ interface EwRiskLevelResult { ewAggregatedRiskResult?.mostRecentDateWithLowRisk } - val daysWithHighRisk: List<LocalDate> - get() = ewAggregatedRiskResult?.exposureWindowDayRisks?.filter { - it.riskLevel.mapToRiskState() == RiskState.INCREASED_RISK - }?.map { it.localDateUtc } ?: emptyList() - - val daysWithLowRisk: List<LocalDate> - get() = ewAggregatedRiskResult?.exposureWindowDayRisks?.filter { - it.riskLevel.mapToRiskState() == RiskState.LOW_RISK - }?.map { it.localDateUtc } ?: emptyList() - enum class FailureReason(val failureCode: String) { UNKNOWN("unknown"), TRACING_OFF("tracingOff"), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index e7891623cc31e9c57bb4b7368d0dc529185655d8..a483530c3c8c1c567fa7b91a69f593074c4b5cea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -16,12 +16,16 @@ import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevel import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedAggregatedRiskPerDateResult import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedRiskResult import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.flow.shareLatest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import org.joda.time.Days +import org.joda.time.Instant import timber.log.Timber import de.rki.coronawarnapp.util.flow.combine as flowCombine @@ -30,6 +34,7 @@ abstract class BaseRiskLevelStorage constructor( private val presenceTracingRiskRepository: PresenceTracingRiskRepository, scope: CoroutineScope, private val riskCombinator: RiskCombinator, + private val timeStamper: TimeStamper, ) : RiskLevelStorage { private val database by lazy { riskResultDatabaseFactory.create() } @@ -37,6 +42,9 @@ abstract class BaseRiskLevelStorage constructor( internal val exposureWindowsTables by lazy { database.exposureWindows() } internal val aggregatedRiskPerDateResultTables by lazy { database.aggregatedRiskPerDate() } + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + abstract val storedResultLimit: Int private suspend fun List<PersistedRiskLevelResultDao>.combineWithWindows( @@ -131,7 +139,7 @@ abstract class BaseRiskLevelStorage constructor( aggregatedRiskPerDateResultTables.allEntries() .map { it.map { persistedAggregatedRiskPerDateResult -> - persistedAggregatedRiskPerDateResult.toAggregatedRiskPerDateResult() + persistedAggregatedRiskPerDateResult.toExposureWindowDayRisk() } } .shareLatest(tag = TAG, scope = scope) @@ -187,15 +195,21 @@ abstract class BaseRiskLevelStorage constructor( override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> get() = combine( allEwRiskLevelResults, - presenceTracingRiskRepository.allEntries() - ) { ewRiskLevelResults, ptRiskLevelResults -> + presenceTracingRiskRepository.allEntries(), + ewDayRiskStates + ) { ewRiskLevelResults, ptRiskLevelResults, ewDayRiskStates -> val combinedResults = riskCombinator .combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } LastCombinedRiskResults( - lastCalculated = combinedResults.firstOrNull() ?: riskCombinator.latestCombinedResult, + lastCalculated = combinedResults.firstOrNull()?.copy( + // need to provide the data here as they are null in EwAggregatedRiskResult + exposureWindowDayRisks = ewDayRiskStates.filter { ewDayRisk -> + ewDayRisk.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) + } + ) ?: riskCombinator.latestCombinedResult, lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: riskCombinator.initialCombinedResult diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt index dcd8a3bcae175261ad7248518243fbf16c6209f6..c12514e22e0fc3e8f35a5da22ca54b41d2a79aa2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt @@ -14,7 +14,7 @@ data class PersistedAggregatedRiskPerDateResult( @ColumnInfo(name = "minimumDistinctEncountersWithLowRisk") val minimumDistinctEncountersWithLowRisk: Int, @ColumnInfo(name = "minimumDistinctEncountersWithHighRisk") val minimumDistinctEncountersWithHighRisk: Int ) { - fun toAggregatedRiskPerDateResult(): ExposureWindowDayRisk = + fun toExposureWindowDayRisk(): ExposureWindowDayRisk = ExposureWindowDayRisk( dateMillisSinceEpoch = dateMillisSinceEpoch, riskLevel = riskLevel, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt index 18ec9e4c29a3c2051546b6fd2c4b2b7118660159..e53e0c16f2affbcb8817c6805e2c728ba25a07f6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt @@ -90,8 +90,6 @@ class CombinedEwPtRiskTest : BaseTest() { minimumDistinctEncountersWithHighRisk = 0 ) - every { ewAggregatedRiskResult.exposureWindowDayRisks } returns listOf(ewDayRisk, ewDayRisk2) - val ptDayRisk = PresenceTracingDayRisk( riskState = RiskState.LOW_RISK, localDateUtc = Instant.ofEpochMilli(1000).toLocalDateUtc() @@ -114,10 +112,49 @@ class CombinedEwPtRiskTest : BaseTest() { ewRiskLevelResult = createEwRiskLevel( calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), ewAggregatedRiskResult - ) + ), + exposureWindowDayRisks = listOf(ewDayRisk, ewDayRisk2) ).daysWithEncounters shouldBe 3 } + @Test + fun `counts days correctly`() { + val dayRisk = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + minimumDistinctEncountersWithLowRisk = 0, + minimumDistinctEncountersWithHighRisk = 1 + ) + val dayRisk2 = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000 + MILLIS_DAY, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + minimumDistinctEncountersWithLowRisk = 1, + minimumDistinctEncountersWithHighRisk = 0 + ) + val dayRisk3 = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000 + 2 * MILLIS_DAY, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + minimumDistinctEncountersWithLowRisk = 1, + minimumDistinctEncountersWithHighRisk = 2 + ) + + val result = CombinedEwPtRiskLevelResult( + ptRiskLevelResult = createPtRiskLevelResult( + calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), + riskState = RiskState.LOW_RISK, + presenceTracingDayRisk = listOf() + ), + ewRiskLevelResult = createEwRiskLevel( + calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), + ewAggregatedRiskResult + ), + exposureWindowDayRisks = listOf(dayRisk, dayRisk2, dayRisk3) + ) + + result.ewDaysWithHighRisk.size shouldBe 2 + result.ewDaysWithLowRisk.size shouldBe 1 + } + private fun createPtRiskLevelResult( calculatedAt: Instant, riskState: RiskState, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt index c2bb7d54632201dc77070fac12757dd674cdf4c5..0a6a80870edd786fa6c38c828921d8edc5aa916c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt @@ -2,12 +2,8 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk -import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.mockk import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach @@ -16,8 +12,6 @@ import testhelpers.BaseTest class EwRiskLevelResultTest : BaseTest() { - @MockK lateinit var ewAggregatedRiskResult1: EwAggregatedRiskResult - @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -36,35 +30,6 @@ class EwRiskLevelResultTest : BaseTest() { ).wasSuccessfullyCalculated shouldBe true } - @Test - fun `counts days correctly`() { - val dayRisk = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000, - riskLevel = RiskLevel.HIGH, - minimumDistinctEncountersWithLowRisk = 0, - minimumDistinctEncountersWithHighRisk = 1 - ) - val dayRisk2 = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000 + MILLIS_DAY, - riskLevel = RiskLevel.LOW, - minimumDistinctEncountersWithLowRisk = 1, - minimumDistinctEncountersWithHighRisk = 0 - ) - val dayRisk3 = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000 + 2 * MILLIS_DAY, - riskLevel = RiskLevel.HIGH, - minimumDistinctEncountersWithLowRisk = 1, - minimumDistinctEncountersWithHighRisk = 2 - ) - every { ewAggregatedRiskResult1.exposureWindowDayRisks } returns listOf(dayRisk, dayRisk2, dayRisk3) - val riskLevel = createRiskLevel( - ewAggregatedRiskResult = ewAggregatedRiskResult1, - failureReason = null - ) - riskLevel.daysWithHighRisk.size shouldBe 2 - riskLevel.daysWithLowRisk.size shouldBe 1 - } - private fun createRiskLevel( ewAggregatedRiskResult: EwAggregatedRiskResult?, failureReason: EwRiskLevelResult.FailureReason? @@ -76,5 +41,3 @@ class EwRiskLevelResultTest : BaseTest() { override val matchedKeyCount: Int = 0 } } - -private const val MILLIS_DAY = (1000 * 60 * 60 * 24).toLong() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index 4ad2bbd28da2b03920fd7d3a785ae6229519c7fd..ead1de9a95156d5baee367764ebaea9d805ae225 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -103,6 +103,7 @@ class BaseRiskLevelStorageTest : BaseTest() { riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, riskCombinator = riskCombinator, + timeStamper = timeStamper, ) { override val storedResultLimit: Int = storedResultLimit @@ -124,7 +125,7 @@ class BaseRiskLevelStorageTest : BaseTest() { val instance = createInstance() val allEntries = instance.aggregatedRiskPerDateResultTables.allEntries() allEntries shouldBe testPersistedAggregatedRiskPerDateResultFlow - allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf( + allEntries.first().map { it.toExposureWindowDayRisk() } shouldBe listOf( testAggregatedRiskPerDateResult ) diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 673ac7b96c843f1c2ed9909a24b0a68e7d383ff9..da4cf340d0a4052cba4455c09485cc118b733c9c 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -35,6 +35,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var timeStamper: TimeStamper private val testRiskLevelResultDao = PersistedRiskLevelResultDao( id = "riskresult-id", @@ -101,6 +102,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, riskCombinator = RiskCombinator(TimeStamper()), + timeStamper = timeStamper, ) @Test diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 5502ead593df03320642893e077e6a6201216d88..75420cce334e89720646c7eca7a028ab6d71d3a8 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -97,7 +97,8 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { scope = TestCoroutineScope(), riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, - riskCombinator = RiskCombinator(TimeStamper()) + riskCombinator = RiskCombinator(TimeStamper()), + timeStamper = TimeStamper(), ) @Test