1
0
mirror of https://github.com/tuskyapp/Tusky synced 2024-12-22 23:08:04 +01:00

modernize tests (#4777)

- use `runTest` instead of `runBlocking`, where possible
- run all Robolectric tests on Api 34 (where we have most users)
- some new testcase for `TimestampUtilsTest`
- move our only instrumented Android Test, `MigrationsTest`, to unit
test so it runs in CI and expand it to test all migrations
- upgrade Robolectric
- removed truth and espresso as they are no longer needed
This commit is contained in:
Konrad Pozniak 2024-12-03 18:46:29 +01:00 committed by GitHub
parent 555ff1ea48
commit 29914f8fd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 553 additions and 359 deletions

View File

@ -93,7 +93,9 @@ android {
}
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
// workaround to have migrations available in unit tests
// https://github.com/robolectric/robolectric/issues/3928#issuecomment-395309991
debug.assets.srcDirs += files("$projectDir/schemas".toString())
}
// Exclude unneeded files added by libraries
@ -183,12 +185,8 @@ dependencies {
testImplementation libs.bundles.mockito
testImplementation libs.mockwebserver
testImplementation libs.androidx.core.testing
testImplementation libs.androidx.room.testing
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.androidx.work.testing
testImplementation libs.truth
testImplementation libs.turbine
androidTestImplementation libs.espresso.core
androidTestImplementation libs.androidx.room.testing
androidTestImplementation libs.androidx.test.junit
}

View File

@ -1,66 +0,0 @@
package com.keylesspalace.tusky
import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.db.AppDatabase
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
const val TEST_DB = "migration_test"
@RunWith(AndroidJUnit4::class)
class MigrationsTest {
@JvmField
@Rule
var helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrateTo11() {
val db = helper.createDatabase(TEST_DB, 10)
val id = 1
val domain = "domain.site"
val token = "token"
val active = true
val accountId = "accountId"
val username = "username"
val values = arrayOf(
id, domain, token, active, accountId, username, "Display Name",
"https://picture.url", true, true, true, true, true, true, true,
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
false, true
)
db.execSQL(
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
"`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," +
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
"`mediaPreviewEnabled`) " +
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
values
)
db.close()
val newDb = helper.runMigrationsAndValidate(TEST_DB, 11, true, AppDatabase.MIGRATION_10_11)
val cursor = newDb.query("SELECT * FROM AccountEntity")
cursor.moveToFirst()
assertEquals(id, cursor.getInt(0))
assertEquals(domain, cursor.getString(1))
assertEquals(token, cursor.getString(2))
assertEquals(active, cursor.getInt(3) != 0)
assertEquals(accountId, cursor.getString(4))
assertEquals(username, cursor.getString(5))
}
}

View File

@ -1,49 +0,0 @@
package android.text
// Used for stubbing Android implementation without slow & buggy Robolectric things
@Suppress("unused")
class SpannableString(private val text: CharSequence) : Spannable {
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) {
throw NotImplementedError()
}
override fun <T : Any?> getSpans(start: Int, end: Int, type: Class<T>?): Array<T> {
throw NotImplementedError()
}
override fun removeSpan(what: Any?) {
throw NotImplementedError()
}
override fun toString(): String {
return "FakeSpannableString[text=$text]"
}
override val length: Int
get() = text.length
override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int {
throw NotImplementedError()
}
override fun getSpanEnd(tag: Any?): Int {
throw NotImplementedError()
}
override fun getSpanFlags(tag: Any?): Int {
throw NotImplementedError()
}
override fun get(index: Int): Char {
throw NotImplementedError()
}
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
throw NotImplementedError()
}
override fun getSpanStart(tag: Any?): Int {
throw NotImplementedError()
}
}

View File

@ -42,7 +42,7 @@ import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class FilterV1Test {

View File

@ -11,7 +11,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class StatusComparisonTest {

View File

@ -67,7 +67,7 @@ import retrofit2.Response
* Created by charlag on 3/7/18.
*/
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class ComposeActivityTest {
private lateinit var activity: ComposeActivity

View File

@ -15,7 +15,7 @@
package com.keylesspalace.tusky.components.compose
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@ -89,7 +89,7 @@ class ComposeTokenizerTest(
@Test
fun tokenIndices_matchExpectations() {
Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
}
}

View File

@ -14,7 +14,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class ComposeViewModelTest {

View File

@ -27,7 +27,7 @@ import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(ParameterizedRobolectricTestRunner::class)
@Config(sdk = [33])
@Config(sdk = [34])
class StatusLengthTest(
private val text: String,
private val expectedLength: Int

View File

@ -38,7 +38,7 @@ import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class NotificationsRemoteMediatorTest {

View File

@ -37,7 +37,7 @@ import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class CachedTimelineRemoteMediatorTest {

View File

@ -4,7 +4,7 @@ import androidx.paging.PagingSource
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@ -12,7 +12,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class NetworkTimelinePagingSourceTest {
@ -23,42 +23,36 @@ class NetworkTimelinePagingSourceTest {
}
@Test
fun `should return empty list when params are Append`() {
fun `should return empty list when params are Append`() = runTest {
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
val params = PagingSource.LoadParams.Append("132", 20, false)
val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null)
runBlocking {
assertEquals(expectedResult, pagingSource.load(params))
}
}
@Test
fun `should return empty list when params are Prepend`() {
fun `should return empty list when params are Prepend`() = runTest {
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
val params = PagingSource.LoadParams.Prepend("132", 20, false)
val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null)
runBlocking {
assertEquals(expectedResult, pagingSource.load(params))
}
}
@Test
fun `should return full list when params are Refresh`() {
fun `should return full list when params are Refresh`() = runTest {
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
val params = PagingSource.LoadParams.Refresh<String>(null, 20, false)
val expectedResult = PagingSource.LoadResult.Page(listOf(status), null, null)
runBlocking {
val result = pagingSource.load(params)
assertEquals(expectedResult, result)
}
}
}

View File

@ -14,7 +14,7 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.viewdata.StatusViewData
import java.io.IOException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import okhttp3.Headers
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals
@ -30,7 +30,7 @@ import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@Config(sdk = [29])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class NetworkTimelineRemoteMediatorTest {
@ -47,7 +47,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should return error when network call returns error code`() {
fun `should return error when network call returns error code`() = runTest {
val timelineViewModel: NetworkTimelineViewModel = mock {
on { statusData } doReturn mutableListOf()
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
@ -55,7 +55,7 @@ class NetworkTimelineRemoteMediatorTest {
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
val result = remoteMediator.load(LoadType.REFRESH, state())
assertTrue(result is RemoteMediator.MediatorResult.Error)
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException)
@ -64,7 +64,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should return error when network call fails`() {
fun `should return error when network call fails`() = runTest {
val timelineViewModel: NetworkTimelineViewModel = mock {
on { statusData } doReturn mutableListOf()
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
@ -72,7 +72,7 @@ class NetworkTimelineRemoteMediatorTest {
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
val result = remoteMediator.load(LoadType.REFRESH, state())
assertTrue(result is RemoteMediator.MediatorResult.Error)
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException)
@ -80,7 +80,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should do initial loading`() {
fun `should do initial loading`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf()
val timelineViewModel: NetworkTimelineViewModel = mock {
@ -111,7 +111,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
val result = remoteMediator.load(LoadType.REFRESH, state)
val newStatusData = mutableListOf(
fakeStatusViewData("7"),
@ -127,7 +127,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should not prepend statuses`() {
fun `should not prepend statuses`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("3"),
fakeStatusViewData("2"),
@ -162,7 +162,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
val result = remoteMediator.load(LoadType.REFRESH, state)
val newStatusData = mutableListOf(
fakeStatusViewData("5"),
@ -179,7 +179,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should refresh and insert placeholder`() {
fun `should refresh and insert placeholder`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("3"),
fakeStatusViewData("2"),
@ -214,7 +214,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
val result = remoteMediator.load(LoadType.REFRESH, state)
val newStatusData = mutableListOf(
fakeStatusViewData("10"),
@ -232,7 +232,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should refresh and not insert placeholders`() {
fun `should refresh and not insert placeholders`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("8"),
fakeStatusViewData("7"),
@ -267,7 +267,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
val result = remoteMediator.load(LoadType.APPEND, state)
val newStatusData = mutableListOf(
fakeStatusViewData("8"),
@ -285,7 +285,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should append statuses`() {
fun `should append statuses`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("8"),
fakeStatusViewData("7"),
@ -324,7 +324,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
val result = remoteMediator.load(LoadType.APPEND, state)
val newStatusData = mutableListOf(
fakeStatusViewData("8"),
@ -342,7 +342,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should not append statuses when pagination end has been reached`() {
fun `should not append statuses when pagination end has been reached`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("8"),
fakeStatusViewData("7"),
@ -370,7 +370,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
val result = remoteMediator.load(LoadType.APPEND, state)
val newStatusData = mutableListOf(
fakeStatusViewData("8"),
@ -385,7 +385,7 @@ class NetworkTimelineRemoteMediatorTest {
@Test
@ExperimentalPagingApi
fun `should not append duplicates for trending statuses`() {
fun `should not append duplicates for trending statuses`() = runTest {
val statuses: MutableList<StatusViewData> = mutableListOf(
fakeStatusViewData("5"),
fakeStatusViewData("4"),
@ -421,7 +421,7 @@ class NetworkTimelineRemoteMediatorTest {
)
)
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
val result = remoteMediator.load(LoadType.APPEND, state)
val newStatusData = mutableListOf(
fakeStatusViewData("5"),

View File

@ -22,7 +22,7 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import java.io.IOException
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@ -35,7 +35,7 @@ import org.mockito.kotlin.stub
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class ViewThreadViewModelTest {
@ -113,12 +113,11 @@ class ViewThreadViewModelTest {
}
@Test
fun `should emit status and context when both load`() {
fun `should emit status and context when both load`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -132,10 +131,9 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should emit status even if context fails to load`() {
fun `should emit status even if context fails to load`() = runTest {
api.stub {
onBlocking { status(threadId) } doReturn NetworkResult.success(fakeStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1"))
onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException())
@ -143,7 +141,6 @@ class ViewThreadViewModelTest {
viewModel.loadThread(threadId)
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -155,10 +152,9 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should emit error when status and context fail to load`() {
fun `should emit error when status and context fail to load`() = runTest {
api.stub {
onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException())
onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException())
@ -166,16 +162,14 @@ class ViewThreadViewModelTest {
viewModel.loadThread(threadId)
runBlocking {
assertEquals(
ThreadUiState.Error::class.java,
viewModel.uiState.first().javaClass
)
}
}
@Test
fun `should emit error when status fails to load`() {
fun `should emit error when status fails to load`() = runTest {
api.stub {
onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException())
onBlocking { statusContext(threadId) } doReturn NetworkResult.success(
@ -188,22 +182,19 @@ class ViewThreadViewModelTest {
viewModel.loadThread(threadId)
runBlocking {
assertEquals(
ThreadUiState.Error::class.java,
viewModel.uiState.first().javaClass
)
}
}
@Test
fun `should update state when reveal button is toggled`() {
fun `should update state when reveal button is toggled`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
viewModel.toggleRevealButton()
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -217,15 +208,13 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should handle status changed event`() {
fun `should handle status changed event`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
runBlocking {
eventHub.dispatch(StatusChangedEvent(fakeStatus(id = "1", spoilerText = "Test", favourited = false)))
assertEquals(
@ -241,17 +230,15 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should remove status`() {
fun `should remove status`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
viewModel.removeStatus(fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test"))
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -264,10 +251,9 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should change status expanded state`() {
fun `should change status expanded state`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
@ -277,7 +263,6 @@ class ViewThreadViewModelTest {
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
)
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -291,10 +276,9 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should change content collapsed state`() {
fun `should change content collapsed state`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
@ -304,7 +288,6 @@ class ViewThreadViewModelTest {
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
)
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -318,10 +301,9 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
@Test
fun `should change content showing state`() {
fun `should change content showing state`() = runTest {
mockSuccessResponses()
viewModel.loadThread(threadId)
@ -331,7 +313,6 @@ class ViewThreadViewModelTest {
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
)
runBlocking {
assertEquals(
ThreadUiState.Success(
statusViewData = listOf(
@ -345,7 +326,6 @@ class ViewThreadViewModelTest {
viewModel.uiState.first()
)
}
}
private fun mockSuccessResponses() {
api.stub {

View File

@ -0,0 +1,87 @@
package com.keylesspalace.tusky.db
import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.di.StorageModule
import com.keylesspalace.tusky.entity.Emoji
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class MigrationsTest {
@get:Rule
val migrationHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun testMigrations() {
/** the db name must match the one in [StorageModule.providesDatabase] */
val db = migrationHelper.createDatabase("tuskyDB", 10)
val moshi = Moshi.Builder().build()
val id = 1L
val domain = "domain.site"
val token = "token"
val active = true
val accountId = "accountId"
val username = "username"
val emoji = moshi.adapter<List<Emoji>>(Types.newParameterizedType(List::class.java, Emoji::class.java), emptySet()).toJson(
listOf(
Emoji(
shortcode = "testemoji",
url = "https://some.url",
staticUrl = "https://some.url",
visibleInPicker = true,
category = null
)
)
)
val values = arrayOf(
id, domain, token, active, accountId, username, "Display Name",
"https://picture.url", true, true, true, true, true, true, true,
true, "1000", "[]", emoji, 0, false,
false, true
)
db.execSQL(
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
"`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," +
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
"`mediaPreviewEnabled`) " +
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
values
)
db.close()
// Room will run all migrations and validate the scheme afterwards
val roomDb = StorageModule.providesDatabase(
InstrumentationRegistry.getInstrumentation().context,
Converters(moshi)
)
val account = roomDb.accountDao().loadAll().first()
roomDb.close()
assertEquals(id, account.id)
assertEquals(domain, account.domain)
assertEquals(token, account.accessToken)
assertEquals(active, account.isActive)
assertEquals(accountId, account.accountId)
assertEquals(username, account.username)
}
}

View File

@ -28,7 +28,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class DatabaseCleanerTest {
private lateinit var timelineDao: TimelineDao

View File

@ -25,7 +25,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class NotificationsDaoTest {
private lateinit var notificationsDao: NotificationsDao

View File

@ -19,7 +19,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class TimelineDaoTest {
private lateinit var timelineDao: TimelineDao

View File

@ -1,46 +1,48 @@
package com.keylesspalace.tusky.entity
import com.keylesspalace.tusky.settings.ProxyConfiguration
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class ProxyConfigurationTest {
@Test
fun `serialized non-int is not valid proxy port`() {
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("should fail"))
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("1.5"))
assertFalse(ProxyConfiguration.isValidProxyPort("should fail"))
assertFalse(ProxyConfiguration.isValidProxyPort("1.5"))
}
@Test
fun `number outside port range is not valid`() {
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}"))
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}"))
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}"))
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}"))
}
@Test
fun `number in port range, inclusive of min and max, is valid`() {
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT))
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT))
Assert.assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2))
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT))
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT))
assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2))
}
@Test
fun `create with invalid port yields null`() {
Assert.assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1))
assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1))
}
@Test
fun `create with invalid hostname yields null`() {
Assert.assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT))
assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT))
}
@Test
fun `create with valid hostname and port yields the config object`() {
Assert.assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
}
@Test
fun `unicode hostname allowed`() {
Assert.assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
}
}

View File

@ -8,7 +8,7 @@ import com.keylesspalace.tusky.appstore.StatusChangedEvent
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import java.util.Date
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals
import org.junit.Before
@ -21,7 +21,7 @@ import org.robolectric.annotation.Config
import retrofit2.HttpException
import retrofit2.Response
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class TimelineCasesTest {
@ -39,23 +39,21 @@ class TimelineCasesTest {
}
@Test
fun `pin success emits StatusChangedEvent`() {
fun `pin success emits StatusChangedEvent`() = runTest {
val pinnedStatus = mockStatus(pinned = true)
api.stub {
onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(pinnedStatus)
}
runBlocking {
eventHub.events.test {
timelineCases.pin(statusId, true)
assertEquals(StatusChangedEvent(pinnedStatus), awaitItem())
}
}
}
@Test
fun `pin failure with server error throws TimelineError with server message`() {
fun `pin failure with server error throws TimelineError with server message`() = runTest {
api.stub {
onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure(
HttpException(
@ -66,13 +64,11 @@ class TimelineCasesTest {
)
)
}
runBlocking {
assertEquals(
"Validation Failed: You have already pinned the maximum number of toots",
timelineCases.pin(statusId, true).exceptionOrNull()?.message
)
}
}
private fun mockStatus(pinned: Boolean = false): Status {
return Status(

View File

@ -1,13 +1,8 @@
package com.keylesspalace.tusky.util
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class HttpHeaderLinkTest {
data class TestData(val name: String, val input: String, val want: List<HttpHeaderLink>)

View File

@ -19,7 +19,7 @@ import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class LinkHelperTest {
private val listener = object : LinkListener {

View File

@ -4,45 +4,47 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.db.entity.AccountEntity
import org.junit.Assert
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class LocaleUtilsTest {
@Test
fun initialLanguagesContainReplySelectedAppAndSystem() {
val expectedLanguages = arrayOf<String?>("yi", "tok", "da", "fr", "sv", "kab")
val languages = getMockedInitialLanguages(expectedLanguages)
Assert.assertArrayEquals(expectedLanguages, languages.subList(0, expectedLanguages.size).toTypedArray())
assertArrayEquals(expectedLanguages, languages.subList(0, expectedLanguages.size).toTypedArray())
}
@Test
fun whenReplyLanguageIsNull_DefaultLanguageIsFirst() {
val defaultLanguage = "tok"
val languages = getMockedInitialLanguages(arrayOf(null, defaultLanguage, "da", "fr", "sv", "kab"))
Assert.assertEquals(defaultLanguage, languages[0])
assertEquals(defaultLanguage, languages[0])
}
@Test
fun initialLanguagesAreDistinct() {
val defaultLanguage = "da"
val languages = getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))
Assert.assertEquals(1, languages.count { it == defaultLanguage })
assertEquals(1, languages.count { it == defaultLanguage })
}
@Test
fun initialLanguageDeduplicationDoesNotReorder() {
val defaultLanguage = "da"
Assert.assertEquals(
assertEquals(
defaultLanguage,
getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0]
)
Assert.assertEquals(
assertEquals(
defaultLanguage,
getMockedInitialLanguages(arrayOf(null, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0]
)
@ -51,7 +53,7 @@ class LocaleUtilsTest {
@Test
fun emptyInitialLanguagesAreDropped() {
val languages = getMockedInitialLanguages(arrayOf("", "", "fr", "", "kab", ""))
Assert.assertFalse(languages.any { it.isEmpty() })
assertFalse(languages.any { it.isEmpty() })
}
private fun getMockedInitialLanguages(configuredLanguages: Array<String?>): List<String> {

View File

@ -3,7 +3,7 @@ package com.keylesspalace.tusky.util
import java.util.Locale
import kotlin.math.pow
import org.junit.AfterClass
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@ -65,6 +65,6 @@ class NumberUtilsTest(private val input: Long, private val want: String) {
@Test
fun test() {
Assert.assertEquals(want, formatNumber(input, 1000))
assertEquals(want, formatNumber(input, 1000))
}
}

View File

@ -10,7 +10,7 @@ import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class RickRollTest {
private lateinit var activity: Activity

View File

@ -8,7 +8,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class SmartLengthInputFilterTest {

View File

@ -1,29 +1,78 @@
package com.keylesspalace.tusky.util
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.R
import androidx.test.platform.app.InstrumentationRegistry
import java.util.Locale
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
private const val STATUS_CREATED_AT_NOW = "test"
@Config(sdk = [28])
@Config(sdk = [34])
@RunWith(AndroidJUnit4::class)
class TimestampUtilsTest {
private val ctx: Context = mock {
on { getString(R.string.status_created_at_now) } doReturn STATUS_CREATED_AT_NOW
companion object {
private lateinit var locale: Locale
@BeforeClass
@JvmStatic
fun beforeClass() {
locale = Locale.getDefault()
Locale.setDefault(Locale.ENGLISH)
}
@AfterClass
@JvmStatic
fun afterClass() {
Locale.setDefault(locale)
}
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
@Test
fun `should return 'now' for small timespans`() {
assertEquals("now", getRelativeTimeSpanString(context, 0, 300))
assertEquals("now", getRelativeTimeSpanString(context, 300, 0))
assertEquals("now", getRelativeTimeSpanString(context, 501, 0))
assertEquals("now", getRelativeTimeSpanString(context, 0, 999))
}
@Test
fun shouldShowNowForSmallTimeSpans() {
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999))
fun `should return 'in --' when then is after now`() {
assertEquals("in 49s", getRelativeTimeSpanString(context, 49.seconds.inWholeMilliseconds, 0))
assertEquals("in 34m", getRelativeTimeSpanString(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds))
assertEquals("in 7h", getRelativeTimeSpanString(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds))
assertEquals("in 10d", getRelativeTimeSpanString(context, 10.days.inWholeMilliseconds, 0))
assertEquals("in 4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds))
}
@Test
fun `should return correct timespans`() {
assertEquals("49s", getRelativeTimeSpanString(context, 0, 49.seconds.inWholeMilliseconds))
assertEquals("34m", getRelativeTimeSpanString(context, 3.minutes.inWholeMilliseconds, 37.minutes.inWholeMilliseconds))
assertEquals("7h", getRelativeTimeSpanString(context, 3.hours.inWholeMilliseconds, 10.hours.inWholeMilliseconds))
assertEquals("10d", getRelativeTimeSpanString(context, 0, 10.days.inWholeMilliseconds))
assertEquals("4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds))
}
@Test
fun `should return correct poll duration`() {
assertEquals("1 second left", formatPollDuration(context, 1.seconds.inWholeMilliseconds, 0))
assertEquals("49 seconds left", formatPollDuration(context, 49.seconds.inWholeMilliseconds, 0))
assertEquals("1 minute left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 36.minutes.inWholeMilliseconds))
assertEquals("34 minutes left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds))
assertEquals("1 hour left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 9.hours.inWholeMilliseconds))
assertEquals("7 hours left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds))
assertEquals("1 day left", formatPollDuration(context, 1.days.inWholeMilliseconds, 0))
assertEquals("10 days left", formatPollDuration(context, 10.days.inWholeMilliseconds, 0))
assertEquals("1460 days left", formatPollDuration(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds))
}
}

View File

@ -28,7 +28,6 @@ conscrypt = "2.5.3"
coroutines = "1.9.0"
diffx = "1.1.1"
emoji2 = "1.4.0"
espresso = "3.6.1"
filemoji-compat = "3.2.7"
glide = "4.16.0"
# Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631
@ -46,10 +45,9 @@ networkresult-calladapter = "1.2.0"
okhttp = "4.12.0"
okio = "3.9.1"
retrofit = "2.11.0"
robolectric = "4.13"
robolectric = "4.14.1"
sparkbutton = "4.2.0"
touchimageview = "3.6"
truth = "1.4.4"
turbine = "1.2.0"
unified-push = "2.4.0"
xmlwriter = "1.0.4"
@ -102,7 +100,6 @@ conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref =
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
diffx = { module = "org.pageseeder.diffx:pso-diffx", version.ref = "diffx" }
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
filemojicompat-core = { module = "de.c1710:filemojicompat", version.ref = "filemoji-compat" }
filemojicompat-defaults = { module = "de.c1710:filemojicompat-defaults", version.ref = "filemoji-compat" }
filemojicompat-ui = { module = "de.c1710:filemojicompat-ui", version.ref = "filemoji-compat" }
@ -131,7 +128,6 @@ retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "ret
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" }
touchimageview = { module = "com.github.MikeOrtiz:TouchImageView", version.ref = "touchimageview" }
truth = { module = "com.google.truth:truth", version.ref = "truth" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
unified-push = { module = "com.github.UnifiedPush:android-connector", version.ref = "unified-push" }
xmlwriter = { module = "org.pageseeder.xmlwriter:pso-xmlwriter", version.ref = "xmlwriter" }

View File

@ -2761,6 +2761,14 @@
<sha256 value="41cebc18bbfee1fef5c0c06aaa1e3cb74658bc25bd720c80719d087da0ad02e7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="monitor" version="1.7.2">
<artifact name="monitor-1.7.2.aar">
<sha256 value="868cc120d10d024b886fa157e1e1eaee0e6a8e5d55e7f765ef41d8fc0fea775b" origin="Generated by Gradle"/>
</artifact>
<artifact name="monitor-1.7.2.pom">
<sha256 value="388fe58cf6062ffcce209853ac5c006d1d6efee9ba76b16fabfb45a7f26521ce" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="runner" version="1.5.2">
<artifact name="runner-1.5.2-sources.jar">
<sha256 value="188f6e40732dda9451d70121a12a31f6411bb3ae598d58193e494ff27e111700" origin="Generated by Gradle"/>
@ -8967,6 +8975,14 @@
<sha256 value="2a01226a8509dbd2b6da8008aaacf6fb4f83198ee1f2666cbabd4a19a3f98197" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotation" version="2.34.0">
<artifact name="error_prone_annotation-2.34.0.jar">
<sha256 value="99f8b53c75a50617d4f9bf45512eda82e7d8e9eb377471d822d3d4c4e034c510" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotation-2.34.0.pom">
<sha256 value="439962cc28110faaf17721c1ff4c588d31ec6f3a1a8bfad035b4215a4c408fdd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.11.0">
<artifact name="error_prone_annotations-2.11.0.pom">
<sha256 value="0261ca01f2d2e9ac2ae2ece75d42c56323b385fb294b6bc943f62ef4e92ddf08" origin="Generated by Gradle"/>
@ -9060,6 +9076,11 @@
<sha256 value="767525d9a81129cd081968382980336327be4162b1e2251a182911daa733c123" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.34.0">
<artifact name="error_prone_parent-2.34.0.pom">
<sha256 value="9db673b33ad310735c6467dd6f6606290e609e867a62e5bad8f9b3365db7188e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="javac-shaded" version="9-dev-r4023-3">
<artifact name="javac-shaded-9-dev-r4023-3.jar">
<sha256 value="65bfccf60986c47fbc17c9ebab0be626afc41741e0a6ec7109e0768817a36f30" origin="Generated by Gradle"/>
@ -9159,6 +9180,17 @@
<sha256 value="452b2d9787b7d366fa8cf5ed9a1c40404542d05effa7a598da03bbbbb76d9f31" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="33.3.1-jre">
<artifact name="guava-33.3.1-android.jar">
<sha256 value="2c3e41d1b380f2044d257947a3aa82dabf3ae4b978622745254aa18b6cf89ab0" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-33.3.1-jre.jar">
<sha256 value="4bf0e2c5af8e4525c96e8fde17a4f7307f97f8478f11c4c8e35a0e3298ae4e90" origin="Generated by Gradle"/>
</artifact>
<artifact name="guava-33.3.1-jre.module">
<sha256 value="41858c84753fd96a6b7c51122fccef39558c91cc08264e08506bcf20e0e63733" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="26.0-android">
<artifact name="guava-parent-26.0-android.pom">
<sha256 value="f8698ab46ca996ce889c1afc8ca4f25eb8ac6b034dc898d4583742360016cc04" origin="Generated by Gradle"/>
@ -9194,6 +9226,11 @@
<sha256 value="5af287a2deb741f38c24a0b390a7a388faee5d7cf0edc4c7b7b98559a5f29fbf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="33.3.1-jre">
<artifact name="guava-parent-33.3.1-jre.pom">
<sha256 value="55441db27e8869dfefe053059bdf478bdc7e95585642bf391f0023345fd56287" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="listenablefuture" version="1.0">
<artifact name="listenablefuture-1.0-sources.jar">
<sha256 value="3d1bd8d4a39b293612a40e547ec51d9ce34fa638d7adeae83871cdbe2923b161" origin="Generated by Gradle"/>
@ -9321,6 +9358,19 @@
<sha256 value="0f022a32490b76449f7404b0506dace458aeaa97c8300b34cfd5be1af1ac992b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.testparameterinjector" name="test-parameter-injector" version="1.18">
<artifact name="test-parameter-injector-1.18.jar">
<sha256 value="e5a7c649c54c412049908247ca5e25fe6921d746849c6017a84dc6044237a4b4" origin="Generated by Gradle"/>
</artifact>
<artifact name="test-parameter-injector-1.18.pom">
<sha256 value="de05732fbdcb5b05e19985abc0a284e41be3ec685387d04f8488a5e6504ea08e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.testparameterinjector" name="test-parameter-injector-parent" version="1.18">
<artifact name="test-parameter-injector-parent-1.18.pom">
<sha256 value="b4aee6be3f6dfafa68309e9da4fb38af7fbc2f8c3e26197451f1db38dc4a85d0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.truth" name="truth" version="1.4.2">
<artifact name="truth-1.4.2-sources.jar">
<sha256 value="9e6e49a3d2eefcadc0294878cb19fa6c6da305f2939c422f3cbd8caf9efe80bb" origin="Generated by Gradle"/>
@ -10623,6 +10673,14 @@
<sha256 value="e0fa622b7de63eae11047ef6e91b4c2ad0f1f0e13cb903ff52080a47f57a5746" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.checkerframework" name="checker-qual" version="3.43.0">
<artifact name="checker-qual-3.43.0.jar">
<sha256 value="3fbc2e98f05854c3df16df9abaa955b91b15b3ecac33623208ed6424640ef0f6" origin="Generated by Gradle"/>
</artifact>
<artifact name="checker-qual-3.43.0.module">
<sha256 value="f8163327245ab8625532948c72a930548cd97f34d6c3fe860fa6aec5a34d79b4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.codehaus.groovy" name="groovy" version="3.0.17">
<artifact name="groovy-3.0.17.jar">
<sha256 value="9ed2a565a8592a9f63f410afed892d0ca6bec1ff96277ce4cc7f3e2e366e794f" origin="Generated by Gradle"/>
@ -12548,6 +12606,14 @@
<sha256 value="de00115f1d84f3a0b2ee3a4b6f6192d066f86d185d67b9d1522f2c80feac5f00" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm" version="9.7.1">
<artifact name="asm-9.7.1.jar">
<sha256 value="8cadd43ac5eb6d09de05faecca38b917a040bb9139c7edeb4cc81c740b713281" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-9.7.1.pom">
<sha256 value="7229b03b30a73ee91008072d9e4569a51d8547fae8c50f527841aef4c1b0baa8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-analysis" version="9.6">
<artifact name="asm-analysis-9.6.jar">
<sha256 value="d92832d7c37edc07c60e2559ac6118b31d642e337a6671edcb7ba9fae68edbbb" origin="Generated by Gradle"/>
@ -12564,6 +12630,14 @@
<sha256 value="9c33080ebcb631ae4f77eb62ed67bfc40cb872e8cfd058ac863e445c1dd973df" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-analysis" version="9.7.1">
<artifact name="asm-analysis-9.7.1.jar">
<sha256 value="85b29371884ba31bb76edf22323c2c24e172c3267a67152eba3d1ccc2e041ef2" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-analysis-9.7.1.pom">
<sha256 value="25c2379f2bfc2a1e64e62c39e2b93cfb0e489707852b08d6fc470b1c6a52b9ee" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-commons" version="9.6">
<artifact name="asm-commons-9.6.jar">
<sha256 value="7aefd0d5c0901701c69f7513feda765fb6be33af2ce7aa17c5781fc87657c511" origin="Generated by Gradle"/>
@ -12580,6 +12654,14 @@
<sha256 value="5acee3ee7252ed90b8074c755d022787499a95fafff98ac4a685107c4da409b4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-commons" version="9.7.1">
<artifact name="asm-commons-9.7.1.jar">
<sha256 value="9a579b54d292ad9be171d4313fd4739c635592c2b5ac3a459bbd1049cddec6a0" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-commons-9.7.1.pom">
<sha256 value="0bf1d31da0c9f9d8edc2f27dbbfdbbf73f1a715b72cd2fa28f3f195994d74ad1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.6">
<artifact name="asm-tree-9.6.jar">
<sha256 value="c43ecf17b539c777e15da7b5b86553b377e2d39a683de6285567d5283888e7ef" origin="Generated by Gradle"/>
@ -12596,6 +12678,14 @@
<sha256 value="a34ea1e3e4128c01038db43c6976e88c779cf5af84b0505da266dfe6965668ec" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.7.1">
<artifact name="asm-tree-9.7.1.jar">
<sha256 value="9929881f59eb6b840e86d54570c77b59ce721d104e6dfd7a40978991c2d3b41f" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-tree-9.7.1.pom">
<sha256 value="13b905f65e7fd43ca7674f40cdaa37679ba4858c6c9d9fb8f17a7afd9baabc9e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-util" version="9.6">
<artifact name="asm-util-9.6.jar">
<sha256 value="c635a7402f4aa9bf66b2f4230cea62025a0fe1cd63e8729adefc9b1994fac4c3" origin="Generated by Gradle"/>
@ -12612,6 +12702,14 @@
<sha256 value="5d014d8c870d4871825bd2ddb5567b21ef6dac8ec48bbb8dbb465b0b3a2bf452" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-util" version="9.7.1">
<artifact name="asm-util-9.7.1.jar">
<sha256 value="f885be71b5c90556f5f1ad1c4f9276b29b96057c497d46666fe4ddbec3cb43c6" origin="Generated by Gradle"/>
</artifact>
<artifact name="asm-util-9.7.1.pom">
<sha256 value="7fb5e63362b2d52d77dca3b754aebad635751d3fc520191e9500ece9e2345b71" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.pageseeder.diffx" name="pso-diffx" version="1.1.1">
<artifact name="pso-diffx-1.1.1.jar">
<sha256 value="c539842b0459fe625062a5ef46cc4449c59f553110cfbdc8c99cc9903bec9690" origin="Generated by Gradle"/>
@ -12644,6 +12742,14 @@
<sha256 value="c422d5c8842d7a01f39b323120b87dec4725337a5b03995df03494a18f88c9a8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="annotations" version="4.14.1">
<artifact name="annotations-4.14.1.jar">
<sha256 value="463a5ad1386c31010bc9af00bfe19a1b758d8c2dedd8c19e5c75d29ab8abb9ba" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotations-4.14.1.module">
<sha256 value="f41e24bf354432477e9022b69843513c4d5a39d722ceb73ee0a508c2889faf4f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="junit" version="4.12.2">
<artifact name="junit-4.12.2.jar">
<sha256 value="f3f6ebf476b4ecdffcfe4e8b3b3d6119cbe2fc59f19822d8a6c7df35dbf22c48" origin="Generated by Gradle"/>
@ -12660,6 +12766,14 @@
<sha256 value="a669f1f50392b26c5d510285663f2e8a870e0a7b875e862474d476147568af5f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="junit" version="4.14.1">
<artifact name="junit-4.14.1.jar">
<sha256 value="2cee817aadce3552706b09450ad1ea7ff5981924072d2adfe40ddad57d5fa123" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-4.14.1.module">
<sha256 value="91e77ccecdbd485c67b84c85728897df116ed3eb90f0e726f0ec79d04768c4d7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="nativeruntime" version="4.12.2">
<artifact name="nativeruntime-4.12.2.jar">
<sha256 value="f8e55ab954404735f5931b5179e139e158367139b7deeddf6ded7086a0467d9d" origin="Generated by Gradle"/>
@ -12676,6 +12790,14 @@
<sha256 value="3fcab094062f01d3a717006603f05d3ee961e843df424f0c3e4e1e6146333d64" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="nativeruntime" version="4.14.1">
<artifact name="nativeruntime-4.14.1.jar">
<sha256 value="c07b66d315aec3272a7c64aa5f154b4194be2cc6030a733d16f2ee87330232a8" origin="Generated by Gradle"/>
</artifact>
<artifact name="nativeruntime-4.14.1.module">
<sha256 value="91b52e58cc84778086941e439659264e3804aeae45df00b47e80962ec9104da3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="nativeruntime-dist-compat" version="1.0.10">
<artifact name="nativeruntime-dist-compat-1.0.10.jar">
<sha256 value="1159b8394b9d9640b53b63f07ec1fe676a7b394a90b8361eef2a88868cca3f3d" origin="Generated by Gradle"/>
@ -12692,6 +12814,14 @@
<sha256 value="4acdac5ceb7e1ed5574abe3510be76180316f60381501884d48cd770e82c33ef" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="nativeruntime-dist-compat" version="1.0.16">
<artifact name="nativeruntime-dist-compat-1.0.16.jar">
<sha256 value="2f4e879b00eed634d0e43353ecff80db4d5ce24b3b213d1e6053cb21b0ced10f" origin="Generated by Gradle"/>
</artifact>
<artifact name="nativeruntime-dist-compat-1.0.16.pom">
<sha256 value="053dc5be36c03495d157186588b73952f4098cb9890f75cbd6ee26e36fd83d04" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="pluginapi" version="4.12.2">
<artifact name="pluginapi-4.12.2.jar">
<sha256 value="822af3ccba184421c36684ec4678e1b1b4424520891d46b1f568c10d3b036ba5" origin="Generated by Gradle"/>
@ -12708,6 +12838,14 @@
<sha256 value="e1e1fcded3313d9962150f1354e5ad2a8f9cdcae170dd23e608584ecba8a00a2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="pluginapi" version="4.14.1">
<artifact name="pluginapi-4.14.1.jar">
<sha256 value="ad8b74238d59bce6631e29190c105dd0c4701e836a2631060678407f621c7b7a" origin="Generated by Gradle"/>
</artifact>
<artifact name="pluginapi-4.14.1.module">
<sha256 value="f1be495b31fa44917a42c85437eb66ba48df2cf8b609e786a5a1694162ecf118" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.12.2">
<artifact name="plugins-maven-dependency-resolver-4.12.2.jar">
<sha256 value="af77ec038d8c9771fed6942fc905520c80231c1aef321ede2869227e9bd8fb08" origin="Generated by Gradle"/>
@ -12724,6 +12862,14 @@
<sha256 value="2a6f81f13e3f8cef1b25db4f8bbe24e8fc0486ffcc5fc9b43694a6bc4cdbe8b5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.14.1">
<artifact name="plugins-maven-dependency-resolver-4.14.1.jar">
<sha256 value="6c8dbc979db0780755e712acab9eeb9396811b2d4dc31a92a5b259f9876205ec" origin="Generated by Gradle"/>
</artifact>
<artifact name="plugins-maven-dependency-resolver-4.14.1.pom">
<sha256 value="21ab442a41f775ba7ddad9f92bbcb6d073ef4822d7c63c7d269b092890be2af0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="resources" version="4.12.2">
<artifact name="resources-4.12.2.jar">
<sha256 value="7a3371613ac23070030566ac590297b301362aba8cba9372acf0f3701cfe5c99" origin="Generated by Gradle"/>
@ -12740,6 +12886,14 @@
<sha256 value="775f068c6b9c5134a7308bbfac5931ac3f2ac84b652577e639a88e6e1ca19cb2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="resources" version="4.14.1">
<artifact name="resources-4.14.1.jar">
<sha256 value="54eb274a47dec9e74cc9b45b9e4335bb5d83857c63b2a49dd8d1993e6321f2c5" origin="Generated by Gradle"/>
</artifact>
<artifact name="resources-4.14.1.module">
<sha256 value="b2ae6c390c2ba8e0cb459106cff79f416bd5ecca1073b4c4144d58a8b1892f89" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="robolectric" version="4.12.2">
<artifact name="robolectric-4.12.2.jar">
<sha256 value="b8eb9d7cf85bb9636f430d68a50768d1a01a747c94b40b8e8a79abe6a3f95f82" origin="Generated by Gradle"/>
@ -12756,6 +12910,14 @@
<sha256 value="1c4ea47a7de2c52150082e2cf99c1fb023148316bf62d4f5919ce9afb0ca1752" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="robolectric" version="4.14.1">
<artifact name="robolectric-4.14.1.jar">
<sha256 value="e0a73d45bceb94a5a0352b66e2120ad4a4b222fe406079130893b6c79d4441d1" origin="Generated by Gradle"/>
</artifact>
<artifact name="robolectric-4.14.1.module">
<sha256 value="cc223907e19a2cd8d434e0981bf2d604b849d90aea182783c5c7e1a40fc6e398" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="sandbox" version="4.12.2">
<artifact name="sandbox-4.12.2.jar">
<sha256 value="8f0c3966ab51d0fca1ede8a4e706440d3783dbf4f6ec5c484fd3fcf0d9b9c9bf" origin="Generated by Gradle"/>
@ -12772,6 +12934,14 @@
<sha256 value="708a128b05c2941d16b3e1d82b4cae3da8c1f54802e79f687d7b8a72addb3fc7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="sandbox" version="4.14.1">
<artifact name="sandbox-4.14.1.jar">
<sha256 value="de361f3de8c08d4488cf156683830f2bd43db1da85a5b136ad6d065b868d3bab" origin="Generated by Gradle"/>
</artifact>
<artifact name="sandbox-4.14.1.module">
<sha256 value="e1e53e16d1a8dc1675711e34108b3e0d71b6a6b7fce02e028c819a9e45b4f3cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadowapi" version="4.12.2">
<artifact name="shadowapi-4.12.2.jar">
<sha256 value="1918a7cfd69f82e2f125a56fa032e3828437a941ec9f077e7a3a652e84cb5cdc" origin="Generated by Gradle"/>
@ -12788,6 +12958,14 @@
<sha256 value="5db21aa2eb063cffa3faae654ff43603f9092d7409e1fadd784e2eed115ddbfe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadowapi" version="4.14.1">
<artifact name="shadowapi-4.14.1.jar">
<sha256 value="fd158863cee488475206ba1d23ff4c7d29be412eccd21b12d9416b0030aa582d" origin="Generated by Gradle"/>
</artifact>
<artifact name="shadowapi-4.14.1.module">
<sha256 value="87b1fef13788f52d7bfced79ee68ef243546c5ac1ce1519274f90410fc77e637" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadows-framework" version="4.12.2">
<artifact name="shadows-framework-4.12.2.jar">
<sha256 value="907dd8bb9966ba4b83a017822449fc8ab31ab0a437b29342cb2cfca1177354e1" origin="Generated by Gradle"/>
@ -12804,6 +12982,14 @@
<sha256 value="bebeb74c7a6543e6d67667906bc6a3cfb0acc080ef9c8003dfd3dcd14abfb718" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadows-framework" version="4.14.1">
<artifact name="shadows-framework-4.14.1.jar">
<sha256 value="f3cf7785eecf9b2e80fbb4caac4c42f63eeea3506e289581c04696a56a494622" origin="Generated by Gradle"/>
</artifact>
<artifact name="shadows-framework-4.14.1.module">
<sha256 value="d45a50ae4255b9248d341ebe57438041451d223b8c32f6754555227281db5c46" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadows-versioning" version="4.12.2">
<artifact name="shadows-versioning-4.12.2.jar">
<sha256 value="86126b5b074cb69c6953666ae467de1fa3b6d33bf794633d5b80a1be97ff4b1a" origin="Generated by Gradle"/>
@ -12828,6 +13014,14 @@
<sha256 value="59d7ac54a24460c884fbb40625dad4f989bba021429d7e055e78574abf688797" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="utils" version="4.14.1">
<artifact name="utils-4.14.1.jar">
<sha256 value="6884eec32c9c9b23d74250e30a5ba9c5ece48784f808feba98f119b215ab59d8" origin="Generated by Gradle"/>
</artifact>
<artifact name="utils-4.14.1.pom">
<sha256 value="dd438f68cdf56644fff3aff48a88561c2bd1850e74334b37de435eeed9d2a1fe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="utils-reflector" version="4.12.2">
<artifact name="utils-reflector-4.12.2.jar">
<sha256 value="7e8a926d88d473a05af7cbef07941e03ddf676e987d70f40096193dba0cf5621" origin="Generated by Gradle"/>
@ -12844,6 +13038,14 @@
<sha256 value="5ebb1c7a2a86ad1699af0e68617513f17bb6a516d24e7936ce7d14a1d66812e8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="utils-reflector" version="4.14.1">
<artifact name="utils-reflector-4.14.1.jar">
<sha256 value="eb8f52cdc24f59ae4cf13369c5bf990354c19c386e3e8f9f5d5cee04d836557b" origin="Generated by Gradle"/>
</artifact>
<artifact name="utils-reflector-4.14.1.module">
<sha256 value="bd14da5907a107813fb0fa0b2c4e02c0f3958a1550f88b872ec57424a9377bb9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.slf4j" name="slf4j-api" version="1.7.30">
<artifact name="slf4j-api-1.7.30.jar">
<sha256 value="cdba07964d1bb40a0761485c6b1e8c2f8fd9eb1d19c53928ac0d7f9510105c57" origin="Generated by Gradle"/>
@ -12901,5 +13103,13 @@
<sha256 value="ae11a3b11dcbac1b6403689e3fd8d82b2ceea55e82dbfc4bb32aededf8ccac8e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.yaml" name="snakeyaml" version="2.3">
<artifact name="snakeyaml-2.3.jar">
<sha256 value="63a76fe66b652360bd4c2c107e6f0258daa7d4bb492008ba8c26fcd230ff9146" origin="Generated by Gradle"/>
</artifact>
<artifact name="snakeyaml-2.3.pom">
<sha256 value="0f5a265a06331b0049e352be32ca322de18b17f3d4dbb70635d40da692e3582f" origin="Generated by Gradle"/>
</artifact>
</component>
</components>
</verification-metadata>