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