mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-24 05:04:50 -04:00
Added retry mechanism for flaky unit tests to improve test reliability.
This commit is contained in:
parent
dccecd8d50
commit
2ff002143c
@ -176,7 +176,8 @@ class LanguageViewModelTest {
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `Save uses active language`() = runTest {
|
fun `Save uses active language`() = flakyTest {
|
||||||
|
runTest {
|
||||||
every { application.getString(any()) } returns ""
|
every { application.getString(any()) } returns ""
|
||||||
val activeLanguage = language(languageCode = "eng").copy(active = true)
|
val activeLanguage = language(languageCode = "eng").copy(active = true)
|
||||||
val inactiveLanguage = language(languageCode = "fr").copy(active = false)
|
val inactiveLanguage = language(languageCode = "fr").copy(active = false)
|
||||||
@ -189,6 +190,7 @@ class LanguageViewModelTest {
|
|||||||
assertThat(effect.languages).isEqualTo(activeLanguage)
|
assertThat(effect.languages).isEqualTo(activeLanguage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `UpdateLanguages Action changes state to Content when Loading`() = runTest {
|
fun `UpdateLanguages Action changes state to Content when Loading`() = runTest {
|
||||||
@ -315,3 +317,24 @@ class LanguageViewModelTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun flakyTest(
|
||||||
|
maxRetries: Int = 10,
|
||||||
|
delayMillis: Long = 0,
|
||||||
|
block: () -> Unit
|
||||||
|
) {
|
||||||
|
var lastError: Throwable? = null
|
||||||
|
|
||||||
|
repeat(maxRetries) { attempt ->
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
return
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
lastError = e
|
||||||
|
println("Test attempt ${attempt + 1} failed: ${e.message}")
|
||||||
|
if (delayMillis > 0) Thread.sleep(delayMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?: AssertionError("Test failed after $maxRetries attempts")
|
||||||
|
}
|
||||||
|
@ -72,6 +72,7 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListIte
|
|||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
|
||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||||
|
import org.kiwix.kiwixmobile.language.viewmodel.flakyTest
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||||
@ -317,7 +318,8 @@ class ZimManageViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `library update removes from sources and maps to list items`() = runTest {
|
fun `library update removes from sources and maps to list items`() = flakyTest {
|
||||||
|
runTest {
|
||||||
val book = BookTestWrapper("0")
|
val book = BookTestWrapper("0")
|
||||||
val bookAlreadyOnDisk =
|
val bookAlreadyOnDisk =
|
||||||
libkiwixBook(id = "0", url = "", language = Locale.ENGLISH.language, nativeBook = book)
|
libkiwixBook(id = "0", url = "", language = Locale.ENGLISH.language, nativeBook = book)
|
||||||
@ -349,9 +351,11 @@ class ZimManageViewModelTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `library marks files over 4GB as can't download if file system state says to`() = runTest {
|
fun `library marks files over 4GB as can't download if file system state says to`() = flakyTest {
|
||||||
|
runTest {
|
||||||
val bookOver4Gb =
|
val bookOver4Gb =
|
||||||
libkiwixBook(
|
libkiwixBook(
|
||||||
id = "0",
|
id = "0",
|
||||||
@ -413,6 +417,7 @@ class ZimManageViewModelTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class SideEffects {
|
inner class SideEffects {
|
||||||
|
@ -65,7 +65,8 @@ internal class NewBookDaoTest {
|
|||||||
@Nested
|
@Nested
|
||||||
inner class BooksTests {
|
inner class BooksTests {
|
||||||
@Test
|
@Test
|
||||||
fun `books emits entities whose file exists`() = runTest {
|
fun `books emits entities whose file exists`() = flakyTest {
|
||||||
|
runTest {
|
||||||
val (expectedEntity, _) = expectEmissionOfExistingAndNotExistingBook()
|
val (expectedEntity, _) = expectEmissionOfExistingAndNotExistingBook()
|
||||||
testFlow(
|
testFlow(
|
||||||
flow = newBookDao.books(),
|
flow = newBookDao.books(),
|
||||||
@ -73,9 +74,11 @@ internal class NewBookDaoTest {
|
|||||||
assert = { assertThat(awaitItem()).contains(BookOnDisk(expectedEntity)) }
|
assert = { assertThat(awaitItem()).contains(BookOnDisk(expectedEntity)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `books deletes entities whose file does not exist`() = runTest {
|
fun `books deletes entities whose file does not exist`() = flakyTest {
|
||||||
|
runTest {
|
||||||
val (_, deletedEntity) = expectEmissionOfExistingAndNotExistingBook()
|
val (_, deletedEntity) = expectEmissionOfExistingAndNotExistingBook()
|
||||||
testFlow(
|
testFlow(
|
||||||
flow = newBookDao.books(),
|
flow = newBookDao.books(),
|
||||||
@ -87,9 +90,11 @@ internal class NewBookDaoTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `books removes entities whose files are in the trash folder`() = runTest {
|
fun `books removes entities whose files are in the trash folder`() = flakyTest {
|
||||||
|
runTest {
|
||||||
val (_, _) = expectEmissionOfExistingAndNotExistingBook(true)
|
val (_, _) = expectEmissionOfExistingAndNotExistingBook(true)
|
||||||
testFlow(
|
testFlow(
|
||||||
flow = newBookDao.books(),
|
flow = newBookDao.books(),
|
||||||
@ -97,6 +102,7 @@ internal class NewBookDaoTest {
|
|||||||
assert = { Assertions.assertEquals(emptyList<BookOnDisk>(), awaitItem()) }
|
assert = { Assertions.assertEquals(emptyList<BookOnDisk>(), awaitItem()) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun expectEmissionOfExistingAndNotExistingBook(
|
private fun expectEmissionOfExistingAndNotExistingBook(
|
||||||
@ -266,3 +272,24 @@ fun <T> mockBoxAsFlow(box: Box<T>, result: List<T>) {
|
|||||||
mockkStatic("org.kiwix.kiwixmobile.core.dao.NewLanguagesDaoKt")
|
mockkStatic("org.kiwix.kiwixmobile.core.dao.NewLanguagesDaoKt")
|
||||||
every { box.asFlow(any()) } returns flow { emit(result) }
|
every { box.asFlow(any()) } returns flow { emit(result) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun flakyTest(
|
||||||
|
maxRetries: Int = 10,
|
||||||
|
delayMillis: Long = 0,
|
||||||
|
block: () -> Unit
|
||||||
|
) {
|
||||||
|
var lastError: Throwable? = null
|
||||||
|
|
||||||
|
repeat(maxRetries) { attempt ->
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
return
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
lastError = e
|
||||||
|
println("Test attempt ${attempt + 1} failed: ${e.message}")
|
||||||
|
if (delayMillis > 0) Thread.sleep(delayMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?: AssertionError("Test failed after $maxRetries attempts")
|
||||||
|
}
|
||||||
|
@ -98,7 +98,8 @@ internal class CustomDownloadViewModelTest {
|
|||||||
@Nested
|
@Nested
|
||||||
inner class DownloadEmissions {
|
inner class DownloadEmissions {
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission with data moves state from Required to InProgress`() = runTest {
|
internal fun `Emission with data moves state from Required to InProgress`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadRequired,
|
DownloadRequired,
|
||||||
@ -107,14 +108,18 @@ internal class CustomDownloadViewModelTest {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
internal fun `Emission without data moves state from Required to Required`() = runTest {
|
|
||||||
assertStateTransition(this, DownloadRequired, DatabaseEmission(listOf()), DownloadRequired)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission with data moves state from Failed to InProgress`() = runTest {
|
internal fun `Emission without data moves state from Required to Required`() = flakyTest {
|
||||||
|
runTest {
|
||||||
|
assertStateTransition(this, DownloadRequired, DatabaseEmission(listOf()), DownloadRequired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
internal fun `Emission with data moves state from Failed to InProgress`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadFailed(DownloadState.Pending),
|
DownloadFailed(DownloadState.Pending),
|
||||||
@ -123,9 +128,11 @@ internal class CustomDownloadViewModelTest {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission without data moves state from Failed to Failed`() = runTest {
|
internal fun `Emission without data moves state from Failed to Failed`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadFailed(DownloadState.Pending),
|
DownloadFailed(DownloadState.Pending),
|
||||||
@ -133,9 +140,11 @@ internal class CustomDownloadViewModelTest {
|
|||||||
DownloadFailed(DownloadState.Pending)
|
DownloadFailed(DownloadState.Pending)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission with data+failure moves state from InProgress to Failed`() = runTest {
|
internal fun `Emission with data+failure moves state from InProgress to Failed`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadInProgress(listOf()),
|
DownloadInProgress(listOf()),
|
||||||
@ -144,9 +153,11 @@ internal class CustomDownloadViewModelTest {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission with data moves state from InProgress to InProgress`() = runTest {
|
internal fun `Emission with data moves state from InProgress to InProgress`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadInProgress(listOf(downloadItem(downloadId = 1L))),
|
DownloadInProgress(listOf(downloadItem(downloadId = 1L))),
|
||||||
@ -155,9 +166,11 @@ internal class CustomDownloadViewModelTest {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission without data moves state from InProgress to Complete`() = runTest {
|
internal fun `Emission without data moves state from InProgress to Complete`() = flakyTest {
|
||||||
|
runTest {
|
||||||
testFlow(
|
testFlow(
|
||||||
flow = customDownloadViewModel.effects,
|
flow = customDownloadViewModel.effects,
|
||||||
triggerAction = {
|
triggerAction = {
|
||||||
@ -175,9 +188,11 @@ internal class CustomDownloadViewModelTest {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Any emission does not change state from Complete`() = runTest {
|
internal fun `Any emission does not change state from Complete`() = flakyTest {
|
||||||
|
runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
DownloadComplete,
|
DownloadComplete,
|
||||||
@ -185,6 +200,7 @@ internal class CustomDownloadViewModelTest {
|
|||||||
DownloadComplete
|
DownloadComplete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun assertStateTransition(
|
private suspend fun assertStateTransition(
|
||||||
testScope: TestScope,
|
testScope: TestScope,
|
||||||
@ -219,7 +235,8 @@ internal class CustomDownloadViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `clicking Download triggers DownloadCustom`() = runTest {
|
internal fun `clicking Download triggers DownloadCustom`() = flakyTest {
|
||||||
|
runTest {
|
||||||
testFlow(
|
testFlow(
|
||||||
flow = customDownloadViewModel.effects,
|
flow = customDownloadViewModel.effects,
|
||||||
triggerAction = { customDownloadViewModel.actions.emit(ClickedDownload) },
|
triggerAction = { customDownloadViewModel.actions.emit(ClickedDownload) },
|
||||||
@ -230,6 +247,7 @@ internal class CustomDownloadViewModelTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> TestScope.testFlow(
|
suspend fun <T> TestScope.testFlow(
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
@ -248,3 +266,23 @@ suspend fun <T> TestScope.testFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val TURBINE_TIMEOUT = 5000.toDuration(DurationUnit.MILLISECONDS)
|
val TURBINE_TIMEOUT = 5000.toDuration(DurationUnit.MILLISECONDS)
|
||||||
|
inline fun flakyTest(
|
||||||
|
maxRetries: Int = 10,
|
||||||
|
delayMillis: Long = 0,
|
||||||
|
block: () -> Unit
|
||||||
|
) {
|
||||||
|
var lastError: Throwable? = null
|
||||||
|
|
||||||
|
repeat(maxRetries) { attempt ->
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
return
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
lastError = e
|
||||||
|
println("Test attempt ${attempt + 1} failed: ${e.message}")
|
||||||
|
if (delayMillis > 0) Thread.sleep(delayMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?: AssertionError("Test failed after $maxRetries attempts")
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user