Skip to content
Snippets Groups Projects
Unverified Commit b3e84723 authored by Chilja Gossow's avatar Chilja Gossow Committed by GitHub
Browse files

Add unit test for PresenceTracingRiskRepository (EXPOSUREAPP-6332) (#2814)


* new tests and refactoring

* new tests and refactoring

* adjust naming

Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: default avatarBMItter <46747780+BMItter@users.noreply.github.com>
parent ba1eefb3
No related branches found
No related tags found
No related merge requests found
...@@ -68,7 +68,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( ...@@ -68,7 +68,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor(
taskRunTime.postValue(duration) taskRunTime.postValue(duration)
val warningPackages = traceWarningRepository.allMetaData.first() val warningPackages = traceWarningRepository.allMetaData.first()
val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() val overlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first()
val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull() val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull()
val infoText = when { val infoText = when {
...@@ -99,7 +99,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( ...@@ -99,7 +99,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor(
riskCalculationRuntime.postValue(it) riskCalculationRuntime.postValue(it)
}, },
{ {
val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() val checkInWarningOverlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first()
val normalizedTimePerCheckInDayList = val normalizedTimePerCheckInDayList =
presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps) presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps)
val riskStates = val riskStates =
......
...@@ -7,44 +7,43 @@ import org.joda.time.Instant ...@@ -7,44 +7,43 @@ import org.joda.time.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
/** /**
* @param presenceTracingDayRisk 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 last calculation, if successful, otherwise null * @param checkInWarningOverlaps Only available for the latest calculation, otherwise null
*/ */
data class PtRiskLevelResult( data class PtRiskLevelResult(
val calculatedAt: Instant, val calculatedAt: Instant,
val riskState: RiskState, val riskState: RiskState,
val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null, private val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null,
private val checkInWarningOverlaps: List<CheckInWarningOverlap>? = null, private val checkInWarningOverlaps: List<CheckInWarningOverlap>? = null,
) { ) {
val wasSuccessfullyCalculated: Boolean val wasSuccessfullyCalculated: Boolean by lazy {
get() = riskState != RiskState.CALCULATION_FAILED riskState != RiskState.CALCULATION_FAILED
}
val numberOfDaysWithHighRisk: Int val numberOfDaysWithHighRisk: Int by lazy {
get() = presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0 presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0
}
val numberOfDaysWithLowRisk: Int val numberOfDaysWithLowRisk: Int by lazy {
get() = presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0 presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0
}
val mostRecentDateWithHighRisk: LocalDate? val mostRecentDateWithHighRisk: LocalDate? by lazy {
get() = presenceTracingDayRisk presenceTracingDayRisk
?.filter { it.riskState == RiskState.INCREASED_RISK } ?.filter { it.riskState == RiskState.INCREASED_RISK }
?.maxByOrNull { it.localDateUtc } ?.maxByOrNull { it.localDateUtc }
?.localDateUtc ?.localDateUtc
}
val mostRecentDateWithLowRisk: LocalDate? val mostRecentDateWithLowRisk: LocalDate? by lazy {
get() = presenceTracingDayRisk presenceTracingDayRisk
?.filter { it.riskState == RiskState.LOW_RISK } ?.filter { it.riskState == RiskState.LOW_RISK }
?.maxByOrNull { it.localDateUtc } ?.maxByOrNull { it.localDateUtc }
?.localDateUtc ?.localDateUtc
}
val daysWithEncounters: Int val checkInOverlapCount: Int by lazy {
get() = when (riskState) { checkInWarningOverlaps?.size ?: 0
RiskState.INCREASED_RISK -> numberOfDaysWithHighRisk }
RiskState.LOW_RISK -> numberOfDaysWithLowRisk
else -> 0
}
val checkInOverlapCount: Int
get() = checkInWarningOverlaps?.size ?: 0
} }
...@@ -38,18 +38,19 @@ class PresenceTracingRiskCalculator @Inject constructor( ...@@ -38,18 +38,19 @@ class PresenceTracingRiskCalculator @Inject constructor(
} }
} }
suspend fun calculateAggregatedRiskPerDay(list: List<CheckInNormalizedTime>): suspend fun calculateDayRisk(
List<PresenceTracingDayRisk> { list: List<CheckInNormalizedTime>
return list.groupBy { it.localDateUtc }.map { ): List<PresenceTracingDayRisk> {
val normalizedTimePerDate = it.value.sumByDouble { return list.groupBy { it.localDateUtc }.map {
it.normalizedTime val normalizedTimePerDate = it.value.sumByDouble {
} it.normalizedTime
PresenceTracingDayRisk(
localDateUtc = it.key,
riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate)
)
} }
PresenceTracingDayRisk(
localDateUtc = it.key,
riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate)
)
} }
}
suspend fun calculateTotalRisk(list: List<CheckInNormalizedTime>): RiskState { suspend fun calculateTotalRisk(list: List<CheckInNormalizedTime>): RiskState {
if (list.isEmpty()) return RiskState.LOW_RISK if (list.isEmpty()) return RiskState.LOW_RISK
......
...@@ -46,27 +46,16 @@ class PresenceTracingRiskRepository @Inject constructor( ...@@ -46,27 +46,16 @@ class PresenceTracingRiskRepository @Inject constructor(
database.presenceTracingRiskLevelResultDao() database.presenceTracingRiskLevelResultDao()
} }
private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches() val overlapsOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches().map { entities ->
.map { timeIntervalMatchEntities -> entities
timeIntervalMatchEntities .map { it.toCheckInWarningOverlap() }
.map { it.toCheckInWarningOverlap() } .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) }
.filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } }
}
val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> =
traceTimeIntervalMatchDao.allMatches().map { matchEntities ->
matchEntities.map {
it.toCheckInWarningOverlap()
}
}
private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map { private val normalizedTimeOfLast14DaysPlusToday = overlapsOfLast14DaysPlusToday.map {
presenceTracingRiskCalculator.calculateNormalizedTime(it) presenceTracingRiskCalculator.calculateNormalizedTime(it)
} }
private val fifteenDaysAgo: Instant
get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration())
val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> =
normalizedTimeOfLast14DaysPlusToday.map { normalizedTimeOfLast14DaysPlusToday.map {
presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it)
...@@ -74,7 +63,7 @@ class PresenceTracingRiskRepository @Inject constructor( ...@@ -74,7 +63,7 @@ class PresenceTracingRiskRepository @Inject constructor(
val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> =
normalizedTimeOfLast14DaysPlusToday.map { normalizedTimeOfLast14DaysPlusToday.map {
presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) presenceTracingRiskCalculator.calculateDayRisk(it)
} }
/** /**
...@@ -120,39 +109,23 @@ class PresenceTracingRiskRepository @Inject constructor( ...@@ -120,39 +109,23 @@ class PresenceTracingRiskRepository @Inject constructor(
} }
fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list ->
var lastSuccessfulFound = false list.sortAndComplementLatestResult()
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,
)
}
}
} }
fun allEntries() = riskLevelResultDao.allEntries().map { list -> fun allEntries() = riskLevelResultDao.allEntries().map { list ->
var lastSuccessfulFound = false list.sortAndComplementLatestResult()
list.sortedByDescending { }
private suspend fun List<PresenceTracingRiskLevelResultEntity>.sortAndComplementLatestResult() =
sortedByDescending {
it.calculatedAtMillis it.calculatedAtMillis
} }
.map { entity -> .mapIndexed { index, entity ->
if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { if (index == 0) {
lastSuccessfulFound = true // add risk per day to the latest result
// add risk per day to the last successful result
entity.toRiskLevelResult( entity.toRiskLevelResult(
presenceTracingDayRisks = presenceTracingDayRisk.first(), presenceTracingDayRisks = presenceTracingDayRisk.first(),
checkInWarningOverlaps = checkInWarningOverlaps.first(), checkInWarningOverlaps = overlapsOfLast14DaysPlusToday.first(),
) )
} else { } else {
entity.toRiskLevelResult( entity.toRiskLevelResult(
...@@ -161,13 +134,15 @@ class PresenceTracingRiskRepository @Inject constructor( ...@@ -161,13 +134,15 @@ class PresenceTracingRiskRepository @Inject constructor(
) )
} }
} }
}
private fun addResult(result: PtRiskLevelResult) { private fun addResult(result: PtRiskLevelResult) {
Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.") Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.")
riskLevelResultDao.insert(result.toRiskLevelEntity()) riskLevelResultDao.insert(result.toRiskLevelEntity())
} }
private val fifteenDaysAgo: Instant
get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration())
suspend fun clearAllTables() { suspend fun clearAllTables() {
traceTimeIntervalMatchDao.deleteAll() traceTimeIntervalMatchDao.deleteAll()
riskLevelResultDao.deleteAll() riskLevelResultDao.deleteAll()
......
...@@ -103,7 +103,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ...@@ -103,7 +103,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() {
) )
runBlockingTest { runBlockingTest {
val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime)) val result = createInstance().calculateDayRisk(listOf(normTime))
result.size shouldBe 1 result.size shouldBe 1
result[0].riskState shouldBe RiskState.CALCULATION_FAILED result[0].riskState shouldBe RiskState.CALCULATION_FAILED
} }
...@@ -133,7 +133,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ...@@ -133,7 +133,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() {
) )
runBlockingTest { runBlockingTest {
val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime, normTime2, normTime3)) val result = createInstance().calculateDayRisk(listOf(normTime, normTime2, normTime3))
result.size shouldBe 2 result.size shouldBe 2
result.find { it.localDateUtc == localDate }!!.riskState shouldBe RiskState.INCREASED_RISK result.find { it.localDateUtc == localDate }!!.riskState shouldBe RiskState.INCREASED_RISK
result.find { it.localDateUtc == localDate2 }!!.riskState shouldBe RiskState.LOW_RISK result.find { it.localDateUtc == localDate2 }!!.riskState shouldBe RiskState.LOW_RISK
......
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
)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment