diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5417dc13d4..49a9a3b72f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -20,6 +20,7 @@ repositories { android { compileSdkVersion 28 + testOptions.unitTests.includeAndroidResources = true defaultConfig { minSdkVersion 21 @@ -45,6 +46,7 @@ dependencies { def support_version = '28.0.0' def moshi_version = '1.8.0' def lifecycle_version = "1.1.1" + def powermock_version = "2.0.0-RC.4" implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -94,7 +96,14 @@ dependencies { testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:4.0.2' + testImplementation 'org.robolectric:shadows-support-v4:3.0' + testImplementation "io.mockk:mockk:1.8.13.kotlin13" + testImplementation 'org.amshove.kluent:kluent-android:1.44' + androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' + } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java deleted file mode 100644 index f9b3c62a4c..0000000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package im.vector.matrix.android; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("im.vector.matrix.android.test", appContext.getPackageName()); - } -} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt new file mode 100644 index 0000000000..c726b7eb0f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -0,0 +1,15 @@ +package im.vector.matrix.android + +import android.content.Context +import android.support.test.InstrumentationRegistry +import java.io.File + +abstract class InstrumentedTest { + fun context(): Context { + return InstrumentationRegistry.getTargetContext() + } + + fun cacheDir(): File { + return context().cacheDir + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt new file mode 100644 index 0000000000..146bc75210 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -0,0 +1,147 @@ +package im.vector.matrix.android.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addAll +import im.vector.matrix.android.internal.database.helper.isUnlinked +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.createObject +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import kotlin.random.Random + + +internal class ChunkEntityTest : InstrumentedTest() { + + private lateinit var monarchy: Monarchy + + @Before + fun setup() { + Realm.init(context()) + val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build() + monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build() + } + + + @Test + fun add_shouldAdd_whenNotAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.events.size shouldEqual 1 + } + } + + @Test + fun add_shouldNotAdd_whenAlreadyIncluded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.events.size shouldEqual 1 + } + } + + @Test + fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(true) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 + } + } + + @Test + fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvent = createFakeEvent(false) + chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 + } + } + + @Test + fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvents = createFakeListOfEvents(30) + val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size + chunk.addAll(fakeEvents, PaginationDirection.FORWARDS) + chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents + } + } + + @Test + fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() { + monarchy.runTransactionSync { realm -> + val chunk: ChunkEntity = realm.createObject() + val fakeEvents = createFakeListOfEvents(30) + val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size + val lastIsState = fakeEvents.last().isStateEvent() + val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents + chunk.addAll(fakeEvents, PaginationDirection.BACKWARDS) + chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex + } + } + + @Test + fun merge_shouldAddEvents_whenMergingBackward() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.events.size shouldEqual 60 + } + } + + @Test + fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.isUnlinked() shouldEqual false + } + } + + @Test + fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() { + monarchy.runTransactionSync { realm -> + val chunk1: ChunkEntity = realm.createObject() + val chunk2: ChunkEntity = realm.createObject() + chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.isUnlinked() shouldEqual true + } + } + + + private fun createFakeListOfEvents(size: Int = 10): List { + return (0 until size).map { createFakeEvent(Random.nextBoolean()) } + } + + private fun createFakeEvent(asStateEvent: Boolean = false): Event { + val eventId = Random.nextLong(System.currentTimeMillis()).toString() + val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE + return Event(type, eventId) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 06a60d5ed5..f8d8207a3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -2,7 +2,6 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity @@ -41,7 +40,7 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, eventsToMerge = chunkToMerge.events } eventsToMerge.forEach { - add(it.asDomain(), direction, isUnlinked = isUnlinked) + add(it, direction, isUnlinked = isUnlinked) } } @@ -63,16 +62,23 @@ internal fun ChunkEntity.add(event: Event, direction: PaginationDirection, stateIndexOffset: Int = 0, isUnlinked: Boolean = false) { + add(event.asEntity(), direction, stateIndexOffset, isUnlinked) +} + +internal fun ChunkEntity.add(eventEntity: EventEntity, + direction: PaginationDirection, + stateIndexOffset: Int = 0, + isUnlinked: Boolean = false) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } - if (event.eventId == null || events.fastContains(event.eventId)) { + if (eventEntity.eventId.isEmpty() || events.fastContains(eventEntity.eventId)) { return } var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) - if (direction == PaginationDirection.FORWARDS && event.isStateEvent()) { + if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) { currentStateIndex += 1 } else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) { val lastEventType = events.last()?.type ?: "" @@ -81,7 +87,6 @@ internal fun ChunkEntity.add(event: Event, } } - val eventEntity = event.asEntity() eventEntity.stateIndex = currentStateIndex eventEntity.isUnlinked = isUnlinked val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size @@ -90,7 +95,7 @@ internal fun ChunkEntity.add(event: Event, internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex - PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex + PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 7dfcc8ab1c..7b5822876d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -69,6 +69,7 @@ internal fun RealmList.find(eventId: String): EventEntity? { return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst() } -internal fun RealmList.fastContains(eventId: String): Boolean { +internal fun RealmList. + fastContains(eventId: String): Boolean { return this.find(eventId) != null } diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java b/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java deleted file mode 100644 index 86ea905e61..0000000000 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package im.vector.matrix.android; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file