Don't hide potential timeline bugs by catching all exceptions (#2372)

* don't hide potential timeline bugs by catching all exceptions

* fix NetworkTimelineRemoteMediatorTest

* improve ifExpected function

* fix code formatting
This commit is contained in:
Konrad Pozniak 2022-03-08 21:39:59 +01:00 committed by GitHub
parent 4d8289b245
commit 34b7a3c8ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 39 additions and 22 deletions

View file

@ -0,0 +1,17 @@
package com.keylesspalace.tusky.components.timeline.util
import retrofit2.HttpException
import java.io.IOException
fun Throwable.isExpected() = this is IOException || this is HttpException
inline fun <T> ifExpected(
t: Throwable,
cb: () -> T
): T {
if (t.isExpected()) {
return cb()
} else {
throw t
}
}

View file

@ -23,6 +23,7 @@ import androidx.room.withTransaction
import com.google.gson.Gson import com.google.gson.Gson
import com.keylesspalace.tusky.components.timeline.Placeholder import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.toEntity import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.TimelineStatusEntity import com.keylesspalace.tusky.db.TimelineStatusEntity
@ -109,7 +110,9 @@ class CachedTimelineRemoteMediator(
} }
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
} catch (e: Exception) { } catch (e: Exception) {
return MediatorResult.Error(e) return ifExpected(e) {
MediatorResult.Error(e)
}
} }
} }

View file

@ -34,6 +34,7 @@ import com.keylesspalace.tusky.appstore.ReblogEvent
import com.keylesspalace.tusky.components.timeline.Placeholder import com.keylesspalace.tusky.components.timeline.Placeholder
import com.keylesspalace.tusky.components.timeline.toEntity import com.keylesspalace.tusky.components.timeline.toEntity
import com.keylesspalace.tusky.components.timeline.toViewData import com.keylesspalace.tusky.components.timeline.toViewData
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
@ -191,7 +192,9 @@ class CachedTimelineViewModel @Inject constructor(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
loadMoreFailed(placeholderId, e) ifExpected(e) {
loadMoreFailed(placeholderId, e)
}
} }
} }
} }

View file

@ -19,6 +19,7 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType import androidx.paging.LoadType
import androidx.paging.PagingState import androidx.paging.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.dec
@ -107,7 +108,9 @@ class NetworkTimelineRemoteMediator(
viewModel.currentSource?.invalidate() viewModel.currentSource?.invalidate()
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
} catch (e: Exception) { } catch (e: Exception) {
return MediatorResult.Error(e) return ifExpected(e) {
MediatorResult.Error(e)
}
} }
} }
} }

View file

@ -28,6 +28,7 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.FavoriteEvent import com.keylesspalace.tusky.appstore.FavoriteEvent
import com.keylesspalace.tusky.appstore.PinEvent import com.keylesspalace.tusky.appstore.PinEvent
import com.keylesspalace.tusky.appstore.ReblogEvent import com.keylesspalace.tusky.appstore.ReblogEvent
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
@ -43,6 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await import kotlinx.coroutines.rx3.await
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -170,7 +172,9 @@ class NetworkTimelineViewModel @Inject constructor(
currentSource?.invalidate() currentSource?.invalidate()
} catch (e: Exception) { } catch (e: Exception) {
loadMoreFailed(placeholderId, e) ifExpected(e) {
loadMoreFailed(placeholderId, e)
}
} }
} }
} }
@ -214,6 +218,7 @@ class NetworkTimelineViewModel @Inject constructor(
currentSource?.invalidate() currentSource?.invalidate()
} }
@Throws(IOException::class, HttpException::class)
suspend fun fetchStatusesForKind( suspend fun fetchStatusesForKind(
fromId: String?, fromId: String?,
uptoId: String?, uptoId: String?,

View file

@ -33,6 +33,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.appstore.ReblogEvent import com.keylesspalace.tusky.appstore.ReblogEvent
import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.appstore.StatusDeletedEvent
import com.keylesspalace.tusky.appstore.UnfollowEvent import com.keylesspalace.tusky.appstore.UnfollowEvent
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Poll
@ -46,8 +47,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlow import kotlinx.coroutines.rx3.asFlow
import kotlinx.coroutines.rx3.await import kotlinx.coroutines.rx3.await
import retrofit2.HttpException
import java.io.IOException
abstract class TimelineViewModel( abstract class TimelineViewModel(
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
@ -291,19 +290,6 @@ abstract class TimelineViewModel(
} }
} }
private fun isExpectedRequestException(t: Exception) = t is IOException || t is HttpException
private inline fun ifExpected(
t: Exception,
cb: () -> Unit
) {
if (isExpectedRequestException(t)) {
cb()
} else {
throw t
}
}
companion object { companion object {
private const val TAG = "TimelineVM" private const val TAG = "TimelineVM"
internal const val LOAD_AT_ONCE = 30 internal const val LOAD_AT_ONCE = 30

View file

@ -27,7 +27,7 @@ import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import java.lang.RuntimeException import java.io.IOException
@Config(sdk = [29]) @Config(sdk = [29])
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -66,7 +66,7 @@ class NetworkTimelineRemoteMediatorTest {
val timelineViewModel: NetworkTimelineViewModel = mock { val timelineViewModel: NetworkTimelineViewModel = mock {
on { statusData } doReturn mutableListOf() on { statusData } doReturn mutableListOf()
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow RuntimeException() onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
} }
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
@ -74,7 +74,7 @@ class NetworkTimelineRemoteMediatorTest {
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) } val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
assertTrue(result is RemoteMediator.MediatorResult.Error) assertTrue(result is RemoteMediator.MediatorResult.Error)
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is RuntimeException) assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException)
} }
@Test @Test