diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt index 7e0e674bcee1e3f48cb962d9a1f286e9ef20f8ba..dfb0d221f958bb44cadceaff6600824147254b68 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt @@ -68,7 +68,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( taskRunTime.postValue(duration) val warningPackages = traceWarningRepository.allMetaData.first() - val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val overlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first() val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull() val infoText = when { @@ -99,7 +99,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( riskCalculationRuntime.postValue(it) }, { - val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val checkInWarningOverlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first() val normalizedTimePerCheckInDayList = presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps) val riskStates = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt index 541496f3bac86227cc1fe7204d8289961871def4..bc9bb66393317c88172eb6889ec8021d322aaab3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt @@ -7,44 +7,43 @@ import org.joda.time.Instant import org.joda.time.LocalDate /** - * @param presenceTracingDayRisk Only available for the last calculation, if successful, otherwise null - * @param checkInWarningOverlaps Only available for the last calculation, if successful, otherwise null + * @param presenceTracingDayRisk Only available for the latest calculation, otherwise null + * @param checkInWarningOverlaps Only available for the latest calculation, otherwise null */ data class PtRiskLevelResult( val calculatedAt: Instant, val riskState: RiskState, - val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null, + private val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null, private val checkInWarningOverlaps: List<CheckInWarningOverlap>? = null, ) { - val wasSuccessfullyCalculated: Boolean - get() = riskState != RiskState.CALCULATION_FAILED + val wasSuccessfullyCalculated: Boolean by lazy { + riskState != RiskState.CALCULATION_FAILED + } - val numberOfDaysWithHighRisk: Int - get() = presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0 + val numberOfDaysWithHighRisk: Int by lazy { + presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0 + } - val numberOfDaysWithLowRisk: Int - get() = presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0 + val numberOfDaysWithLowRisk: Int by lazy { + presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0 + } - val mostRecentDateWithHighRisk: LocalDate? - get() = presenceTracingDayRisk + val mostRecentDateWithHighRisk: LocalDate? by lazy { + presenceTracingDayRisk ?.filter { it.riskState == RiskState.INCREASED_RISK } ?.maxByOrNull { it.localDateUtc } ?.localDateUtc + } - val mostRecentDateWithLowRisk: LocalDate? - get() = presenceTracingDayRisk + val mostRecentDateWithLowRisk: LocalDate? by lazy { + presenceTracingDayRisk ?.filter { it.riskState == RiskState.LOW_RISK } ?.maxByOrNull { it.localDateUtc } ?.localDateUtc + } - val daysWithEncounters: Int - get() = when (riskState) { - RiskState.INCREASED_RISK -> numberOfDaysWithHighRisk - RiskState.LOW_RISK -> numberOfDaysWithLowRisk - else -> 0 - } - - val checkInOverlapCount: Int - get() = checkInWarningOverlaps?.size ?: 0 + val checkInOverlapCount: Int by lazy { + checkInWarningOverlaps?.size ?: 0 + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt index 568273ff125bcc1716ec8bc64d9ead1a201bc339..a2f9f0098e99fa5077a23dd43be7f48e11e76246 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt @@ -38,18 +38,19 @@ class PresenceTracingRiskCalculator @Inject constructor( } } - suspend fun calculateAggregatedRiskPerDay(list: List<CheckInNormalizedTime>): - List<PresenceTracingDayRisk> { - return list.groupBy { it.localDateUtc }.map { - val normalizedTimePerDate = it.value.sumByDouble { - it.normalizedTime - } - PresenceTracingDayRisk( - localDateUtc = it.key, - riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate) - ) + suspend fun calculateDayRisk( + list: List<CheckInNormalizedTime> + ): List<PresenceTracingDayRisk> { + return list.groupBy { it.localDateUtc }.map { + val normalizedTimePerDate = it.value.sumByDouble { + it.normalizedTime } + PresenceTracingDayRisk( + localDateUtc = it.key, + riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate) + ) } + } suspend fun calculateTotalRisk(list: List<CheckInNormalizedTime>): RiskState { if (list.isEmpty()) return RiskState.LOW_RISK diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt index 9dbccbc3e1b0c6c2a191cccde25f7eff674f2d82..1b356f35b51531d19136a1098fd3706e5b1193d8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt @@ -46,27 +46,16 @@ class PresenceTracingRiskRepository @Inject constructor( database.presenceTracingRiskLevelResultDao() } - private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches() - .map { timeIntervalMatchEntities -> - timeIntervalMatchEntities - .map { it.toCheckInWarningOverlap() } - .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } - } - - val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> = - traceTimeIntervalMatchDao.allMatches().map { matchEntities -> - matchEntities.map { - it.toCheckInWarningOverlap() - } - } + val overlapsOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches().map { entities -> + entities + .map { it.toCheckInWarningOverlap() } + .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } + } - private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map { + private val normalizedTimeOfLast14DaysPlusToday = overlapsOfLast14DaysPlusToday.map { presenceTracingRiskCalculator.calculateNormalizedTime(it) } - private val fifteenDaysAgo: Instant - get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) - val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = normalizedTimeOfLast14DaysPlusToday.map { presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) @@ -74,7 +63,7 @@ class PresenceTracingRiskRepository @Inject constructor( val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = normalizedTimeOfLast14DaysPlusToday.map { - presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) + presenceTracingRiskCalculator.calculateDayRisk(it) } /** @@ -120,39 +109,23 @@ class PresenceTracingRiskRepository @Inject constructor( } fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> - var lastSuccessfulFound = false - list.sortedByDescending { - it.calculatedAtMillis - } - .map { entity -> - if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { - lastSuccessfulFound = true - // add risk per day to the last successful result - entity.toRiskLevelResult( - presenceTracingDayRisks = presenceTracingDayRisk.first(), - checkInWarningOverlaps = checkInWarningOverlaps.first(), - ) - } else { - entity.toRiskLevelResult( - presenceTracingDayRisks = null, - checkInWarningOverlaps = null, - ) - } - } + list.sortAndComplementLatestResult() } fun allEntries() = riskLevelResultDao.allEntries().map { list -> - var lastSuccessfulFound = false - list.sortedByDescending { + list.sortAndComplementLatestResult() + } + + private suspend fun List<PresenceTracingRiskLevelResultEntity>.sortAndComplementLatestResult() = + sortedByDescending { it.calculatedAtMillis } - .map { entity -> - if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { - lastSuccessfulFound = true - // add risk per day to the last successful result + .mapIndexed { index, entity -> + if (index == 0) { + // add risk per day to the latest result entity.toRiskLevelResult( presenceTracingDayRisks = presenceTracingDayRisk.first(), - checkInWarningOverlaps = checkInWarningOverlaps.first(), + checkInWarningOverlaps = overlapsOfLast14DaysPlusToday.first(), ) } else { entity.toRiskLevelResult( @@ -161,13 +134,15 @@ class PresenceTracingRiskRepository @Inject constructor( ) } } - } private fun addResult(result: PtRiskLevelResult) { Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.") riskLevelResultDao.insert(result.toRiskLevelEntity()) } + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + suspend fun clearAllTables() { traceTimeIntervalMatchDao.deleteAll() riskLevelResultDao.deleteAll() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt index f59cd16228473db4a9530fa15857c09c758b048a..b0bfef2136687cb4c6cd327fe96b42c47814a1e3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt @@ -103,7 +103,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ) runBlockingTest { - val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime)) + val result = createInstance().calculateDayRisk(listOf(normTime)) result.size shouldBe 1 result[0].riskState shouldBe RiskState.CALCULATION_FAILED } @@ -133,7 +133,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ) runBlockingTest { - val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime, normTime2, normTime3)) + val result = createInstance().calculateDayRisk(listOf(normTime, normTime2, normTime3)) result.size shouldBe 2 result.find { it.localDateUtc == localDate }!!.riskState shouldBe RiskState.INCREASED_RISK result.find { it.localDateUtc == localDate2 }!!.riskState shouldBe RiskState.LOW_RISK diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab4464a2e41681105bb35eda245bdb5a662ba7dd --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt @@ -0,0 +1,255 @@ +package de.rki.coronawarnapp.presencetracing.risk.storage + +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInNormalizedTime +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInRiskPerDay +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Days +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PresenceTracingRiskRepositoryTest : BaseTest() { + + @MockK lateinit var presenceTracingRiskCalculator: PresenceTracingRiskCalculator + @MockK lateinit var databaseFactory: PresenceTracingRiskDatabase.Factory + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var traceTimeIntervalMatchDao: TraceTimeIntervalMatchDao + @MockK lateinit var riskLevelResultDao: PresenceTracingRiskLevelResultDao + @MockK lateinit var database: PresenceTracingRiskDatabase + + private val now = Instant.ofEpochMilli(9999999) + private val fifteenDaysAgo = now.minus(Days.days(15).toStandardDuration()) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns now + + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(emptyList()) + coEvery { traceTimeIntervalMatchDao.insert(any()) } just Runs + coEvery { traceTimeIntervalMatchDao.deleteMatchesForPackage(any()) } just Runs + coEvery { traceTimeIntervalMatchDao.deleteAll() } just Runs + coEvery { traceTimeIntervalMatchDao.deleteOlderThan(any()) } just Runs + + every { riskLevelResultDao.insert(any()) } just Runs + coEvery { riskLevelResultDao.deleteOlderThan(any()) } just Runs + + coEvery { databaseFactory.create() } returns database + every { database.traceTimeIntervalMatchDao() } returns traceTimeIntervalMatchDao + every { database.presenceTracingRiskLevelResultDao() } returns riskLevelResultDao + + coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(any()) } returns listOf() + coEvery { presenceTracingRiskCalculator.calculateTotalRisk(any()) } returns RiskState.LOW_RISK + } + + @Test + fun `overlapsOfLast14DaysPlusToday works`() { + val entity = TraceTimeIntervalMatchEntity( + checkInId = 1L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = fifteenDaysAgo.minus(100000).millis, + endTimeMillis = fifteenDaysAgo.millis + ) + val entity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity, entity2)) + runBlockingTest { + val overlaps = createInstance().overlapsOfLast14DaysPlusToday.first() + overlaps.size shouldBe 1 + overlaps[0].checkInId shouldBe 2L + } + } + + @Test + fun `traceLocationCheckInRiskStates works`() { + val entity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity2)) + val time = CheckInNormalizedTime( + checkInId = 2L, + localDateUtc = now.minus(100000).toLocalDateUtc(), + normalizedTime = 20.0 + ) + val riskPerDay = CheckInRiskPerDay( + checkInId = 2L, + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(listOf(entity2.toCheckInWarningOverlap())) } returns listOf( + time + ) + coEvery { presenceTracingRiskCalculator.calculateCheckInRiskPerDay(listOf(time)) } returns listOf(riskPerDay) + runBlockingTest { + val riskStates = createInstance().traceLocationCheckInRiskStates.first() + riskStates.size shouldBe 1 + riskStates[0].checkInId shouldBe 2L + riskStates[0].riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `presenceTracingDayRisk works`() { + val dayRisk = PresenceTracingDayRisk( + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk) + runBlockingTest { + val risks = createInstance().presenceTracingDayRisk.first() + risks.size shouldBe 1 + risks[0].riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `latestEntries works`() { + val resultEntity = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.minus(100000).millis, + riskState = RiskState.LOW_RISK + ) + val resultEntity2 = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.minus(10000).millis, + riskState = RiskState.LOW_RISK + ) + coEvery { riskLevelResultDao.latestEntries(2) } returns flowOf(listOf(resultEntity, resultEntity2)) + val matchEntity = TraceTimeIntervalMatchEntity( + checkInId = 1L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.millis + ) + val matchEntity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(matchEntity, matchEntity2)) + val dayRisk = PresenceTracingDayRisk( + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk) + runBlockingTest { + val latest = createInstance().latestEntries(2).first() + latest.size shouldBe 2 + latest[0].calculatedAt shouldBe now.minus(10000) + latest[0].checkInOverlapCount shouldBe 2 + latest[1].calculatedAt shouldBe now.minus(100000) + latest[1].checkInOverlapCount shouldBe 0 + } + } + + @Test + fun `deleteStaleData works`() { + runBlockingTest { + createInstance().deleteStaleData() + coVerify { + traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) + riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) + } + } + } + + @Test + fun `deleteAllMatches works`() { + runBlockingTest { + createInstance().deleteAllMatches() + coVerify { traceTimeIntervalMatchDao.deleteAll() } + } + } + + @Test + fun `report successful calculation works`() { + val traceWarningPackageId = "traceWarningPackageId" + val overlap = CheckInWarningOverlap( + checkInId = 1L, + transmissionRiskLevel = 1, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(9991000), + endTime = Instant.ofEpochMilli(9997000) + ) + + val result = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.millis, + riskState = RiskState.LOW_RISK + ) + runBlockingTest { + createInstance().reportCalculation( + successful = true, + overlaps = listOf(overlap) + ) + + coVerify { + traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId) + traceTimeIntervalMatchDao.insert(listOf(overlap.toTraceTimeIntervalMatchEntity())) + riskLevelResultDao.insert(result) + } + } + } + + @Test + fun `report failed calculation works`() { + val traceWarningPackageId = "traceWarningPackageId" + val overlap = CheckInWarningOverlap( + checkInId = 1L, + transmissionRiskLevel = 1, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(9991000), + endTime = Instant.ofEpochMilli(9997000) + ) + + val result = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.millis, + riskState = RiskState.CALCULATION_FAILED + ) + runBlockingTest { + createInstance().reportCalculation( + successful = false, + overlaps = listOf(overlap) + ) + + coVerify { + traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId) + traceTimeIntervalMatchDao.insert(any()) + riskLevelResultDao.insert(result) + } + } + } + + private fun createInstance() = PresenceTracingRiskRepository( + presenceTracingRiskCalculator, + databaseFactory, + timeStamper + ) +}