Refactor tracker and add tests
parent
3edfd0892a
commit
c82bacb037
@ -0,0 +1,160 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.domain
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koin.test.KoinTest
|
||||||
|
import org.koin.test.inject
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class TrackerTest : KoinTest {
|
||||||
|
|
||||||
|
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
private val mangaAdapter = moshi.adapter(Manga::class.java)
|
||||||
|
private val historyRegistry by inject<HistoryRepository>()
|
||||||
|
private val repository by inject<TrackingRepository>()
|
||||||
|
private val dataRepository by inject<MangaDataRepository>()
|
||||||
|
private val tracker by inject<Tracker>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun noUpdates() = runTest {
|
||||||
|
val manga = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(manga.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(manga, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||||
|
tracker.checkUpdates(manga, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasUpdates() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badIds() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaBad = loadManga("bad_ids.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badIds2() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaBad = loadManga("bad_ids.json")
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fullReset() = runTest {
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaEmpty = loadManga("empty.json")
|
||||||
|
tracker.deleteTrack(mangaFull.id)
|
||||||
|
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadManga(name: String): Manga {
|
||||||
|
val assets = InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
val manga = assets.open("manga/$name").use {
|
||||||
|
mangaAdapter.fromJson(it.source().buffer())
|
||||||
|
} ?: throw RuntimeException("Cannot read manga from json \"$name\"")
|
||||||
|
dataRepository.storeManga(manga)
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||||
|
|
||||||
|
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
|
||||||
|
id = trackLog.id,
|
||||||
|
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
||||||
|
manga = manga.toManga(tags.toMangaTags()),
|
||||||
|
createdAt = Date(trackLog.createdAt)
|
||||||
|
)
|
||||||
@ -1,9 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "track_logs",
|
tableName = "track_logs",
|
||||||
@ -1,8 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
|
||||||
class TrackLogWithManga(
|
class TrackLogWithManga(
|
||||||
@Embedded val trackLog: TrackLogEntity,
|
@Embedded val trackLog: TrackLogEntity,
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class TracksDao {
|
abstract class TracksDao {
|
||||||
@ -1,132 +1,115 @@
|
|||||||
package org.koitharu.kotatsu.tracker.domain
|
package org.koitharu.kotatsu.tracker.domain
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaTracking
|
import androidx.annotation.VisibleForTesting
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||||
|
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||||
|
import org.koitharu.kotatsu.tracker.work.TrackingItem
|
||||||
|
|
||||||
class Tracker(
|
class Tracker(
|
||||||
|
private val settings: AppSettings,
|
||||||
private val repository: TrackingRepository,
|
private val repository: TrackingRepository,
|
||||||
|
private val channels: TrackerNotificationChannels,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
|
suspend fun getAllTracks(): List<TrackingItem> {
|
||||||
val repo = MangaRepository(track.manga.source)
|
val sources = settings.trackSources
|
||||||
val details = repo.getDetails(track.manga)
|
if (sources.isEmpty()) {
|
||||||
val chapters = details.chapters.orEmpty()
|
return emptyList()
|
||||||
if (track.isEmpty()) {
|
|
||||||
// first check or manga was empty on last check
|
|
||||||
if (commit) {
|
|
||||||
repository.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = chapters.size,
|
|
||||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
previousTrackChapterId = 0L,
|
|
||||||
newChapters = emptyList(),
|
|
||||||
saveTrackLog = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return MangaUpdates(
|
|
||||||
manga = details,
|
|
||||||
newChapters = emptyList(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val newChapters = details.getNewChapters(track.lastChapterId)
|
|
||||||
if (newChapters.isEmpty()) {
|
|
||||||
if (commit) {
|
|
||||||
repository.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = chapters.size,
|
|
||||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
previousTrackChapterId = 0L,
|
|
||||||
newChapters = emptyList(),
|
|
||||||
saveTrackLog = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return MangaUpdates(
|
|
||||||
manga = details,
|
|
||||||
newChapters = emptyList(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return when {
|
val knownIds = HashSet<Manga>()
|
||||||
|
val result = ArrayList<TrackingItem>()
|
||||||
// the same chapters count
|
// Favourites
|
||||||
chapters.size == track.knownChaptersCount -> {
|
if (AppSettings.TRACK_FAVOURITES in sources) {
|
||||||
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
val favourites = repository.getAllFavouritesManga()
|
||||||
// manga was not updated. skip
|
channels.updateChannels(favourites.keys)
|
||||||
MangaUpdates(
|
for ((category, mangaList) in favourites) {
|
||||||
manga = details,
|
if (!category.isTrackingEnabled || mangaList.isEmpty()) {
|
||||||
newChapters = emptyList(),
|
continue
|
||||||
)
|
}
|
||||||
|
val categoryTracks = repository.getTracks(mangaList)
|
||||||
|
val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
|
||||||
|
channels.getFavouritesChannelId(category.id)
|
||||||
} else {
|
} else {
|
||||||
// number of chapters still the same, bu last chapter changed.
|
null
|
||||||
// maybe some chapters are removed. we need to find last known chapter
|
}
|
||||||
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
|
for (track in categoryTracks) {
|
||||||
if (knownChapter == -1) {
|
if (knownIds.add(track.manga)) {
|
||||||
// confuse. reset anything
|
result.add(TrackingItem(track, channelId))
|
||||||
if (commit) {
|
}
|
||||||
repository.storeTrackResult(
|
}
|
||||||
mangaId = track.manga.id,
|
}
|
||||||
knownChaptersCount = chapters.size,
|
}
|
||||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
// History
|
||||||
previousTrackChapterId = 0L,
|
if (AppSettings.TRACK_HISTORY in sources) {
|
||||||
newChapters = emptyList(),
|
val history = repository.getAllHistoryManga()
|
||||||
saveTrackLog = false,
|
val historyTracks = repository.getTracks(history)
|
||||||
)
|
val channelId = if (channels.isHistoryNotificationsEnabled()) {
|
||||||
}
|
channels.getHistoryChannelId()
|
||||||
MangaUpdates(
|
|
||||||
manga = details,
|
|
||||||
newChapters = emptyList(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val newChapters = chapters.takeLast(chapters.size - knownChapter + 1)
|
null
|
||||||
if (commit) {
|
|
||||||
repository.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = knownChapter + 1,
|
|
||||||
lastChapterId = track.lastChapterId,
|
|
||||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
|
||||||
newChapters = newChapters,
|
|
||||||
saveTrackLog = true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
MangaUpdates(
|
for (track in historyTracks) {
|
||||||
manga = details,
|
if (knownIds.add(track.manga)) {
|
||||||
newChapters = details.getNewChapters(track.lastNotifiedChapterId),
|
result.add(TrackingItem(track, channelId))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
result.trimToSize()
|
||||||
val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount)
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun gc() {
|
||||||
|
repository.gc()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
|
||||||
|
val manga = MangaRepository(track.manga.source).getDetails(track.manga)
|
||||||
|
val updates = compare(track, manga)
|
||||||
if (commit) {
|
if (commit) {
|
||||||
repository.storeTrackResult(
|
repository.saveUpdates(updates)
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = track.knownChaptersCount,
|
|
||||||
lastChapterId = track.lastChapterId,
|
|
||||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
|
||||||
newChapters = newChapters,
|
|
||||||
saveTrackLog = true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
MangaUpdates(
|
return updates
|
||||||
manga = details,
|
|
||||||
newChapters = details.getNewChapters(track.lastNotifiedChapterId),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates {
|
||||||
|
val track = repository.getTrack(manga)
|
||||||
|
val updates = compare(track, manga)
|
||||||
|
if (commit) {
|
||||||
|
repository.saveUpdates(updates)
|
||||||
}
|
}
|
||||||
|
return updates
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Manga.getNewChapters(lastChapterId: Long): List<MangaChapter> {
|
@VisibleForTesting
|
||||||
val chapters = chapters ?: return emptyList()
|
suspend fun deleteTrack(mangaId: Long) {
|
||||||
if (lastChapterId == 0L) {
|
repository.deleteTrack(mangaId)
|
||||||
return emptyList()
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main functionality of tracker: check new chapters in [manga] comparing to the [track]
|
||||||
|
*/
|
||||||
|
private fun compare(track: MangaTracking, manga: Manga): MangaUpdates {
|
||||||
|
if (track.isEmpty()) {
|
||||||
|
// first check or manga was empty on last check
|
||||||
|
return MangaUpdates(manga, emptyList(), isValid = false)
|
||||||
|
}
|
||||||
|
val chapters = requireNotNull(manga.chapters)
|
||||||
|
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
||||||
|
return when {
|
||||||
|
newChapters.isEmpty() -> {
|
||||||
|
return MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
|
||||||
|
}
|
||||||
|
newChapters.size == chapters.size -> {
|
||||||
|
return MangaUpdates(manga, emptyList(), isValid = false)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return MangaUpdates(manga, newChapters, isValid = true)
|
||||||
}
|
}
|
||||||
val raw = chapters.takeLastWhile { x -> x.id != lastChapterId }
|
|
||||||
return if (raw.isEmpty() || raw.size == chapters.size) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
raw
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.tracker.domain.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
data class TrackingLogItem(
|
data class TrackingLogItem(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.TestDispatcher
|
|
||||||
import kotlinx.coroutines.test.resetMain
|
|
||||||
import kotlinx.coroutines.test.setMain
|
|
||||||
import org.junit.rules.TestWatcher
|
|
||||||
import org.junit.runner.Description
|
|
||||||
|
|
||||||
class CoroutineTestRule(
|
|
||||||
private val testDispatcher: TestDispatcher = StandardTestDispatcher(),
|
|
||||||
) : TestWatcher() {
|
|
||||||
|
|
||||||
override fun starting(description: Description) {
|
|
||||||
super.starting(description)
|
|
||||||
Dispatchers.setMain(testDispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finished(description: Description) {
|
|
||||||
super.finished(description)
|
|
||||||
Dispatchers.resetMain()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
|
|
||||||
runBlocking(testDispatcher) {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue