diff --git a/buildSrc/src/main/kotlin/wakatimeapp.android.feature.gradle b/buildSrc/src/main/kotlin/wakatimeapp.android.feature.gradle index d5c661f5..2207fa62 100644 --- a/buildSrc/src/main/kotlin/wakatimeapp.android.feature.gradle +++ b/buildSrc/src/main/kotlin/wakatimeapp.android.feature.gradle @@ -82,6 +82,7 @@ dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") testImplementation("io.mockk:mockk:1.13.2") testImplementation("io.kotest:kotest-assertions-core:5.5.0") + testImplementation("io.kotest.extensions:kotest-assertions-arrow:1.2.5") androidTestImplementation("androidx.test.ext:junit:1.1.3") diff --git a/core/common/src/main/java/com/jacob/wakatimeapp/core/common/Utils.kt b/core/common/src/main/java/com/jacob/wakatimeapp/core/common/Utils.kt index 97b53feb..15f76fb8 100644 --- a/core/common/src/main/java/com/jacob/wakatimeapp/core/common/Utils.kt +++ b/core/common/src/main/java/com/jacob/wakatimeapp/core/common/Utils.kt @@ -2,7 +2,13 @@ package com.jacob.wakatimeapp.core.common // ktlint-disable filename import java.time.format.TextStyle.SHORT import java.util.Locale +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +val LocalDate.Companion.today + get() = Instant.DISTANT_PAST.toLocalDateTime(TimeZone.currentSystemDefault()).date fun LocalDate.getDisplayNameForDay(): String = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()) diff --git a/home/src/main/java/com/jacob/wakatimeapp/home/data/local/HomePageCache.kt b/home/src/main/java/com/jacob/wakatimeapp/home/data/local/HomePageCache.kt index 4caa9692..6978927e 100644 --- a/home/src/main/java/com/jacob/wakatimeapp/home/data/local/HomePageCache.kt +++ b/home/src/main/java/com/jacob/wakatimeapp/home/data/local/HomePageCache.kt @@ -14,7 +14,6 @@ import com.jacob.wakatimeapp.home.domain.models.Last7DaysStats import com.jacob.wakatimeapp.home.domain.models.StreakRange import javax.inject.Inject import javax.inject.Singleton -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -44,7 +43,7 @@ class HomePageCache @Inject constructor( } } - fun getLast7DaysStats(): Flow> = dataStore.data.map { + fun getLast7DaysStats() = dataStore.data.map { val stringUiData = it[KEY_LAST_7_DAYS_STATS] ?: return@map emptyCacheError json.decodeFromString(stringUiData).right() }.catch { @@ -58,9 +57,10 @@ class HomePageCache @Inject constructor( } } - fun getCurrentStreak() = dataStore.data.map { - val stringCurrentStreak = it[KEY_CURRENT_STREAK] ?: return@map emptyCacheError - json.decodeFromString(stringCurrentStreak).right() + fun getCurrentStreak() = dataStore.data.map> { + val streakRange = it[KEY_CURRENT_STREAK]?.let(json::decodeFromString) + ?: StreakRange.ZERO + streakRange.right() }.catch { Timber.e(it) emit(DatabaseError.UnknownError(it.message!!, it).left()) diff --git a/home/src/main/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUC.kt b/home/src/main/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUC.kt index 99ebada9..8cbb6df7 100644 --- a/home/src/main/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUC.kt +++ b/home/src/main/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUC.kt @@ -1,7 +1,10 @@ package com.jacob.wakatimeapp.home.domain.usecases import arrow.core.Either +import arrow.core.computations.either +import com.jacob.wakatimeapp.core.common.today import com.jacob.wakatimeapp.core.models.Error +import com.jacob.wakatimeapp.core.models.Time import com.jacob.wakatimeapp.home.data.local.HomePageCache import com.jacob.wakatimeapp.home.domain.models.StreakRange import javax.inject.Inject @@ -9,6 +12,10 @@ import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate @Singleton class CalculateCurrentStreakUC @Inject constructor( @@ -16,7 +23,35 @@ class CalculateCurrentStreakUC @Inject constructor( private val homePageCache: HomePageCache, ) { - operator fun invoke(): Flow> { - TODO() + operator fun invoke(): Flow> = channelFlow { + val currentStreakFlow = homePageCache.getCurrentStreak() + val last7DaysStatsFlow = homePageCache.getLast7DaysStats() + + launch { currentStreakFlow.collect { send(it) } } + + last7DaysStatsFlow.collect { last7DaysStatsEither -> + either { + val last7DaysStats = last7DaysStatsEither.bind() + val currentStreak = currentStreakFlow.first().bind() + + val todaysStats = last7DaysStats.weeklyTimeSpent[LocalDate.today] ?: Time.ZERO + + if (todaysStats == Time.ZERO) return@either + + val updatedStreakRange = if (currentStreak == StreakRange.ZERO) { + StreakRange( + start = LocalDate.today, + end = LocalDate.today, + ) + } else { + StreakRange( + start = currentStreak.start, + end = LocalDate.today, + ) + } + + homePageCache.updateCurrentStreak(updatedStreakRange) + } + } } } diff --git a/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCRobot.kt b/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCRobot.kt index cd7fce72..2e7d7636 100644 --- a/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCRobot.kt +++ b/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCRobot.kt @@ -1,12 +1,15 @@ package com.jacob.wakatimeapp.home.domain.usecases import arrow.core.Either +import com.jacob.wakatimeapp.core.common.today import com.jacob.wakatimeapp.core.models.Error +import com.jacob.wakatimeapp.core.models.Time import com.jacob.wakatimeapp.home.data.local.HomePageCache import com.jacob.wakatimeapp.home.domain.models.Last7DaysStats import com.jacob.wakatimeapp.home.domain.models.StreakRange +import io.kotest.assertions.arrow.core.shouldBeLeft +import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.matchers.collections.shouldContain -import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe import io.mockk.clearMocks import io.mockk.coEvery @@ -16,6 +19,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.datetime.LocalDate @OptIn(ExperimentalCoroutinesApi::class) internal class CalculateCurrentStreakUCRobot { @@ -27,6 +31,7 @@ internal class CalculateCurrentStreakUCRobot { fun buildUseCase() = apply { clearMocks(mockCache) + results.clear() useCase = CalculateCurrentStreakUC( dispatcher = UnconfinedTestDispatcher(), @@ -46,8 +51,20 @@ internal class CalculateCurrentStreakUCRobot { results shouldContain expected } - fun resultsShouldContain(expected: List>) = apply { - results shouldContainExactly expected + fun resultsShouldContain(expected: StreakRange) = apply { + results.first() shouldBeRight expected + } + + fun resultShouldBeRight() = apply { + results.first().shouldBeRight() + } + + fun resultsShouldContain(expected: Error) = apply { + results.first() shouldBeLeft expected + } + + fun resultShouldBeLeft() = apply { + results.first().shouldBeLeft() } fun mockGetCurrentStreak(data: Either) = apply { @@ -58,7 +75,21 @@ internal class CalculateCurrentStreakUCRobot { coEvery { mockCache.getLast7DaysStats() } returns flowOf(data) } - fun verifyUpdateCurrentStreakCalled(data: StreakRange) = apply { - coVerify { mockCache.updateCurrentStreak(data) } + fun verifyUpdateCurrentStreakCalled(data: StreakRange, count: Int = 1) = apply { + coVerify(exactly = count) { mockCache.updateCurrentStreak(data) } + } + + companion object { + val streakRange = StreakRange.ZERO + val today = LocalDate.today + + val last7DaysStats = Last7DaysStats( + timeSpentToday = Time.ZERO, + projectsWorkedOn = listOf(), + weeklyTimeSpent = mutableMapOf(), + mostUsedLanguage = "", + mostUsedEditor = "", + mostUsedOs = "" + ) } } diff --git a/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCTest.kt b/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCTest.kt new file mode 100644 index 00000000..9075327b --- /dev/null +++ b/home/src/test/java/com/jacob/wakatimeapp/home/domain/usecases/CalculateCurrentStreakUCTest.kt @@ -0,0 +1,52 @@ +package com.jacob.wakatimeapp.home.domain.usecases + +import arrow.core.right +import com.jacob.wakatimeapp.core.models.Time +import com.jacob.wakatimeapp.home.domain.models.StreakRange +import com.jacob.wakatimeapp.home.domain.usecases.CalculateCurrentStreakUCRobot.Companion.last7DaysStats +import com.jacob.wakatimeapp.home.domain.usecases.CalculateCurrentStreakUCRobot.Companion.streakRange +import com.jacob.wakatimeapp.home.domain.usecases.CalculateCurrentStreakUCRobot.Companion.today +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class CalculateCurrentStreakUCTest { + private val robot = CalculateCurrentStreakUCRobot() + + @Test + internal fun `when there is no value in the cache, then set current streak value to 0`() = + runTest { + val map = mutableMapOf() + for (i in 0 until 7) { + map[today.minus(DatePeriod(years = 0, months = 0, days = i))] = Time.ZERO + } + + val updatedLast7DaysStats = last7DaysStats.copy(weeklyTimeSpent = map) + + robot.buildUseCase() + .mockGetCurrentStreak(StreakRange.ZERO.right()) + .mockGetLast7DaysStats(updatedLast7DaysStats.right()) + .callUseCase() + .resultSizeShouldBe(1) + .resultsShouldContain(streakRange.right()) + } + + @Test + internal fun `when there is a value in the cache, then increase the value by 1`() { + TODO("Not yet implemented") + } + + @Test + internal fun `when last 7 days stats cache sends multiple values, then streak should only increase by 1`() { + TODO("Not yet implemented") + } + + @Test + internal fun `when there is non 0 value in cache and todays stats arrives later, then streak should increase by 1`() { + TODO("Not yet implemented") + } +}