Start testing timeline. Not working at the moment, have to figure it out.

This commit is contained in:
ganfra 2018-12-14 19:23:49 +01:00
parent 9af466c24b
commit faa68f4d52
8 changed files with 363 additions and 7 deletions

View File

@ -10,7 +10,7 @@ buildscript {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:5.8.0"
classpath "io.realm:realm-gradle-plugin:5.7.0"
}
}

@ -42,12 +42,6 @@ android {
adbOptions {
installOptions "-g"
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

}

dependencies {
@ -115,12 +109,17 @@ dependencies {
testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "android.arch.core:core-testing:$lifecycle_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "com.android.support.test:rules:1.0.2"
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "android.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"


}

View File

@ -0,0 +1,195 @@
package im.vector.matrix.android;

import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

public final class LiveDataTestObserver<T> implements Observer<T> {
private final List<T> valueHistory = new ArrayList<>();
private final List<Observer<T>> childObservers = new ArrayList<>();

@Deprecated // will be removed in version 1.0
private final LiveData<T> observedLiveData;

private CountDownLatch valueLatch = new CountDownLatch(1);

private LiveDataTestObserver(LiveData<T> observedLiveData) {
this.observedLiveData = observedLiveData;
}

@Override
public void onChanged(@Nullable T value) {
valueHistory.add(value);
valueLatch.countDown();
for (Observer<T> childObserver : childObservers) {
childObserver.onChanged(value);
}
}

public T value() {
assertHasValue();
return valueHistory.get(valueHistory.size() - 1);
}

public List<T> valueHistory() {
return Collections.unmodifiableList(valueHistory);
}

/**
* Disposes and removes observer from observed live data.
*
* @return This Observer
* @deprecated Please use {@link LiveData#removeObserver(Observer)} instead, will be removed in 1.0
*/
@Deprecated
public LiveDataTestObserver<T> dispose() {
observedLiveData.removeObserver(this);
return this;
}

public LiveDataTestObserver<T> assertHasValue() {
if (valueHistory.isEmpty()) {
throw fail("Observer never received any value");
}

return this;
}

public LiveDataTestObserver<T> assertNoValue() {
if (!valueHistory.isEmpty()) {
throw fail("Expected no value, but received: " + value());
}

return this;
}

public LiveDataTestObserver<T> assertHistorySize(int expectedSize) {
int size = valueHistory.size();
if (size != expectedSize) {
throw fail("History size differ; Expected: " + expectedSize + ", Actual: " + size);
}
return this;
}

public LiveDataTestObserver<T> assertValue(T expected) {
T value = value();

if (expected == null && value == null) {
return this;
}

if (!value.equals(expected)) {
throw fail("Expected: " + valueAndClass(expected) + ", Actual: " + valueAndClass(value));
}

return this;
}

public LiveDataTestObserver<T> assertValue(Function<T, Boolean> valuePredicate) {
T value = value();

if (!valuePredicate.apply(value)) {
throw fail("Value not present");
}

return this;
}

public LiveDataTestObserver<T> assertNever(Function<T, Boolean> valuePredicate) {
int size = valueHistory.size();
for (int valueIndex = 0; valueIndex < size; valueIndex++) {
T value = this.valueHistory.get(valueIndex);
if (valuePredicate.apply(value)) {
throw fail("Value at position " + valueIndex + " matches predicate "
+ valuePredicate.toString() + ", which was not expected.");
}
}

return this;
}

/**
* Awaits until this TestObserver has any value.
* <p>
* If this TestObserver has already value then this method returns immediately.
*
* @return this
* @throws InterruptedException if the current thread is interrupted while waiting
*/
public LiveDataTestObserver<T> awaitValue() throws InterruptedException {
valueLatch.await();
return this;
}

/**
* Awaits the specified amount of time or until this TestObserver has any value.
* <p>
* If this TestObserver has already value then this method returns immediately.
*
* @return this
* @throws InterruptedException if the current thread is interrupted while waiting
*/
public LiveDataTestObserver<T> awaitValue(long timeout, TimeUnit timeUnit) throws InterruptedException {
valueLatch.await(timeout, timeUnit);
return this;
}

/**
* Awaits until this TestObserver receives next value.
* <p>
* If this TestObserver has already value then it awaits for another one.
*
* @return this
* @throws InterruptedException if the current thread is interrupted while waiting
*/
public LiveDataTestObserver<T> awaitNextValue() throws InterruptedException {
return withNewLatch().awaitValue();
}


/**
* Awaits the specified amount of time or until this TestObserver receives next value.
* <p>
* If this TestObserver has already value then it awaits for another one.
*
* @return this
* @throws InterruptedException if the current thread is interrupted while waiting
*/
public LiveDataTestObserver<T> awaitNextValue(long timeout, TimeUnit timeUnit) throws InterruptedException {
return withNewLatch().awaitValue(timeout, timeUnit);
}

private LiveDataTestObserver<T> withNewLatch() {
valueLatch = new CountDownLatch(1);
return this;
}

private AssertionError fail(String message) {
return new AssertionError(message);
}

private static String valueAndClass(Object value) {
if (value != null) {
return value + " (class: " + value.getClass().getSimpleName() + ")";
}
return "null";
}

public static <T> LiveDataTestObserver<T> create() {
return new LiveDataTestObserver<>(new MutableLiveData<T>());
}

public static <T> LiveDataTestObserver<T> test(LiveData<T> liveData) {
LiveDataTestObserver<T> observer = new LiveDataTestObserver<>(liveData);
liveData.observeForever(observer);
return observer;
}
}

View File

@ -0,0 +1,6 @@
package im.vector.matrix.android

import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers.Main

internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main)

View File

@ -0,0 +1,24 @@
package im.vector.matrix.android.session.room.timeline

import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import kotlin.random.Random

internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {

override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEvent> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(
Random.nextLong(System.currentTimeMillis()).toString(),
Random.nextLong(System.currentTimeMillis()).toString(),
fakeEvents
)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
.map { tokenChunkEvent }
}


}

View File

@ -0,0 +1,19 @@
package im.vector.matrix.android.session.room.timeline

import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import kotlin.random.Random

internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {

override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
.map { tokenChunkEvent }
}

}

View File

@ -0,0 +1,10 @@
package im.vector.matrix.android.session.room.timeline

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent

internal data class FakeTokenChunkEvent(override val start: String?,
override val end: String?,
override val events: List<Event> = emptyList(),
override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent

View File

@ -0,0 +1,43 @@
package im.vector.matrix.android.session.room.timeline

import com.zhuinden.monarchy.Monarchy
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.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.kotlin.createObject
import kotlin.random.Random

object RoomDataHelper {

fun createFakeListOfEvents(size: Int = 10): List<Event> {
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
}

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)
}

fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = MyMembership.JOINED
val eventList = createFakeListOfEvents(30)
val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLast = true
}
chunkEntity.addAll(eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity)
}
}


}

View File

@ -0,0 +1,60 @@
package im.vector.matrix.android.session.room.timeline

import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.support.test.annotation.UiThreadTest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver
import im.vector.matrix.android.api.thread.MainThreadExecutor
import im.vector.matrix.android.internal.TaskExecutor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Rule
import org.junit.Test

internal class TimelineHolderTest : InstrumentedTest {

@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var monarchy: Monarchy

@Before
fun setup() {
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
}

@Test
@UiThreadTest
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
val roomId = "roomId"
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))

RoomDataHelper.fakeInitialSync(monarchy, roomId)
val timelineHolder = DefaultTimelineHolder(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask)
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
timelineObserver.awaitNextValue().assertHasValue()
var pagedList = timelineObserver.value()
pagedList.size shouldEqual 30
(0 until pagedList.size).map {
pagedList.loadAround(it)
}
timelineObserver.awaitNextValue().assertHasValue()
pagedList = timelineObserver.value()
pagedList.size shouldEqual 60
}


}