Fix duplicate items after running automatic and manual sync at the same time (#1197)

This commit is contained in:
Mikołaj Pich 2021-03-07 20:47:18 +01:00 committed by GitHub
parent af8108a649
commit f14346ff32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 308 additions and 13 deletions

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@ -27,9 +28,12 @@ class AttendanceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance"
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -20,9 +21,12 @@ class AttendanceSummaryRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance_summary"
fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class CompletedLessonsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "completed"
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -20,9 +21,12 @@ class ConferenceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "conference"
fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { conferenceDb.loadAll(semester.diaryId, student.studentId) },
fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class ExamRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "exam"
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) },
fetch = {

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDateTime
import javax.inject.Inject
import javax.inject.Singleton
@ -28,14 +29,20 @@ class GradeRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "grade"
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
gradeDb.loadAll(semester.semesterId, semester.studentId).combine(gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)) { details, summaries ->
details to summaries
}
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries }
},
fetch = {
val (details, summary) = sdk.init(student)
@ -92,19 +99,27 @@ class GradeRepository @Inject constructor(
}
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isRead } }
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isRead }
}
}
fun getNotNotifiedGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isNotified } }
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isNotified }
}
}
fun getNotNotifiedPredictedGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } }
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified }
}
}
fun getNotNotifiedFinalGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } }
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified }
}
}
suspend fun updateGrade(grade: Grade) {

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@ -30,11 +31,16 @@ class GradeStatisticsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val partialMutex = Mutex()
private val semesterMutex = Mutex()
private val pointsMutex = Mutex()
private val partialCacheKey = "grade_stats_partial"
private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = partialMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
@ -71,6 +77,7 @@ class GradeStatisticsRepository @Inject constructor(
)
fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = semesterMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
@ -112,6 +119,7 @@ class GradeStatisticsRepository @Inject constructor(
)
fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = pointsMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -24,9 +25,12 @@ class HomeworkRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "homework"
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) },
fetch = {

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDate.now
import javax.inject.Inject
@ -20,7 +21,10 @@ class LuckyNumberRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },

View File

@ -20,6 +20,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -33,10 +34,13 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
@Suppress("UNUSED_PARAMETER")
fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) },

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class MobileDeviceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "devices"
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class NoteRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "note"
fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { noteDb.loadAll(student.studentId) },
fetch = {

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -16,8 +17,11 @@ class SchoolRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -16,8 +17,11 @@ class StudentInfoRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +18,10 @@ class SubjectRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +18,10 @@ class TeacherRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -31,9 +32,12 @@ class TimetableRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "timetable"
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = {
timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)

View File

@ -13,8 +13,11 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
inline fun <ResultType, RequestType> networkBoundResource(
mutex: Mutex = Mutex(),
showSavedOnLoading: Boolean = true,
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend (ResultType) -> RequestType,
@ -31,7 +34,7 @@ inline fun <ResultType, RequestType> networkBoundResource(
try {
val newData = fetch(data)
saveFetchResult(data, newData)
mutex.withLock { saveFetchResult(query().first(), newData) }
query().map { Resource.success(filterResult(it)) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
@ -44,11 +47,12 @@ inline fun <ResultType, RequestType> networkBoundResource(
@JvmName("networkBoundResourceWithMap")
inline fun <ResultType, RequestType, T> networkBoundResource(
mutex: Mutex = Mutex(),
showSavedOnLoading: Boolean = true,
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend (ResultType) -> RequestType,
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { Unit },
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T
) = flow {
@ -59,7 +63,8 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
if (showSavedOnLoading) emit(Resource.loading(mapResult(data)))
try {
saveFetchResult(data, fetch(data))
val newData = fetch(data)
mutex.withLock { saveFetchResult(query().first(), newData) }
query().map { Resource.success(mapResult(it)) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)

View File

@ -87,6 +87,7 @@ class AttendanceRepositoryTest {
coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList
coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3)
@ -114,6 +115,7 @@ class AttendanceRepositoryTest {
coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1)
coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester))
)
coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3)

View File

@ -87,6 +87,7 @@ class CompletedLessonsRepositoryTest {
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3)
@ -114,6 +115,7 @@ class CompletedLessonsRepositoryTest {
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList.dropLast(1)
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester))
)
coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3)

View File

@ -88,6 +88,7 @@ class ExamRemoteTest {
coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList
coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3)
@ -115,6 +116,7 @@ class ExamRemoteTest {
coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList.dropLast(1)
coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester))
)
coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3)

View File

@ -57,7 +57,7 @@ class GradeRepositoryTest {
coEvery { gradeDb.deleteAll(any()) } just Runs
coEvery { gradeDb.insertAll(any()) } returns listOf()
coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf(flowOf(listOf()), flowOf(listOf()))
coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf(flowOf(listOf()), flowOf(listOf()), flowOf(listOf()))
coEvery { gradeSummaryDb.deleteAll(any()) } just Runs
coEvery { gradeSummaryDb.insertAll(any()) } returns listOf()
}
@ -76,7 +76,8 @@ class GradeRepositoryTest {
coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf(
flowOf(listOf()), // empty because it is new user
flowOf(remoteList.mapToEntities(semester))
flowOf(listOf()), // empty again, after fetch end before save result
flowOf(remoteList.mapToEntities(semester)),
)
// execute
@ -114,6 +115,7 @@ class GradeRepositoryTest {
)
coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf(
flowOf(localList.mapToEntities(semester)),
flowOf(localList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
@ -155,6 +157,7 @@ class GradeRepositoryTest {
)
coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf(
flowOf(localList.mapToEntities(semester)),
flowOf(localList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
@ -184,6 +187,7 @@ class GradeRepositoryTest {
)
coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf(
flowOf(localList.mapToEntities(semester)),
flowOf(localList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
@ -209,6 +213,7 @@ class GradeRepositoryTest {
coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf(
flowOf(listOf()),
flowOf(listOf()), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)

View File

@ -72,6 +72,7 @@ class LuckyNumberRemoteTest {
coEvery { sdk.getLuckyNumber(student.schoolShortName) } returns luckyNumber
coEvery { luckyNumberDb.load(1, date) } returnsMany listOf(
flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)),
flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), // after fetch end before save result
flowOf(luckyNumber.mapToEntity(student))
)
coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3)
@ -101,6 +102,7 @@ class LuckyNumberRemoteTest {
coEvery { sdk.getLuckyNumber(student.schoolShortName) } returns luckyNumber
coEvery { luckyNumberDb.load(1, date) } returnsMany listOf(
flowOf(null),
flowOf(null), // after fetch end before save result
flowOf(luckyNumber.mapToEntity(student))
)
coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3)

View File

@ -82,6 +82,7 @@ class MobileDeviceRepositoryTest {
coEvery { sdk.getRegisteredDevices() } returns remoteList
coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3)
@ -109,6 +110,7 @@ class MobileDeviceRepositoryTest {
coEvery { sdk.getRegisteredDevices() } returns remoteList.dropLast(1)
coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester))
)
coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3)

View File

@ -0,0 +1,192 @@
package io.github.wulkanowy.utils
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class FlowUtilsKtTest {
private val testScope = TestCoroutineScope()
@Test
fun `fetch from two places with same remote data`() {
val repo = mockk<TestRepo>()
coEvery { repo.query() } returnsMany listOf(
// initial data
flowOf(listOf(1, 2, 3)),
flowOf(listOf(1, 2, 3)),
// for first
flowOf(listOf(1, 2, 3)), // before save
flowOf(listOf(2, 3, 4)), // after save
// for second
flowOf(listOf(2, 3, 4)), // before save
flowOf(listOf(2, 3, 4)), // after save
)
coEvery { repo.fetch() } returnsMany listOf(
listOf(2, 3, 4),
listOf(2, 3, 4),
)
coEvery { repo.save(any(), any()) } just Runs
// first
networkBoundResource(
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
delay(2_000)
data
},
saveFetchResult = { old, new -> repo.save(old, new) }
).launchIn(testScope)
testScope.advanceTimeBy(1_000)
// second
networkBoundResource(
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
delay(2_000)
data
},
saveFetchResult = { old, new -> repo.save(old, new) }
).launchIn(testScope)
testScope.advanceTimeBy(3_000)
coVerifyOrder {
// from first
repo.query()
repo.fetch() // hang for 2 sec
// wait 1 sec
// from second
repo.query()
repo.fetch() // hang for 2 sec
// from first
repo.query()
repo.save(withArg {
assertEquals(listOf(1, 2, 3), it)
}, any())
repo.query()
// from second
repo.query()
repo.save(withArg {
assertEquals(listOf(2, 3, 4), it)
}, any())
repo.query()
}
}
@Test
fun `fetch from two places with same remote data and save at the same moment`() {
val repo = mockk<TestRepo>()
coEvery { repo.query() } returnsMany listOf(
// initial data
flowOf(listOf(1, 2, 3)),
flowOf(listOf(1, 2, 3)),
// for first
flowOf(listOf(1, 2, 3)), // before save
flowOf(listOf(2, 3, 4)), // after save
// for second
flowOf(listOf(2, 3, 4)), // before save
flowOf(listOf(2, 3, 4)), // after save
)
coEvery { repo.fetch() } returnsMany listOf(
listOf(2, 3, 4),
listOf(2, 3, 4),
)
coEvery { repo.save(any(), any()) } just Runs
val saveResultMutex = Mutex()
// first
networkBoundResource(
mutex = saveResultMutex,
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
delay(2_000)
data
},
saveFetchResult = { old, new ->
delay(1_500)
repo.save(old, new)
}
).launchIn(testScope)
testScope.advanceTimeBy(1_000)
// second
networkBoundResource(
mutex = saveResultMutex,
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
delay(2_000)
data
},
saveFetchResult = { old, new ->
repo.save(old, new)
}
).launchIn(testScope)
testScope.advanceTimeBy(3_000)
coVerifyOrder {
// from first
repo.query()
repo.fetch() // hang for 2 sec
// wait 1 sec
// from second
repo.query()
repo.fetch() // hang for 2 sec
// from first
repo.query()
repo.save(withArg {
assertEquals(listOf(1, 2, 3), it)
}, any())
// from second
repo.query()
repo.save(withArg {
assertEquals(listOf(2, 3, 4), it)
}, any())
repo.query()
repo.query()
}
}
@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
private class TestRepo {
fun query() = flowOf<List<Int>>()
suspend fun fetch() = listOf<Int>()
suspend fun save(old: List<Int>, new: List<Int>) {}
}
}