From e482ef4262aef95a9dfa1b9f0ede967032c31e2e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com> Date: Mon, 3 Jan 2022 16:51:12 +0200 Subject: [PATCH] First local thread integration test --- .github/workflows/integration.yml | 4 +- .../android/sdk/common/CommonTestHelper.kt | 59 +++++++++- .../threads/GenerateThreadMessageTests.kt | 107 ++++++++++++++++++ .../sdk/session/room/threads/RetryTestRule.kt | 58 ++++++++++ 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 1f087e1ff1..c18ca69fde 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -69,8 +69,8 @@ jobs: python3 -m venv .synapse source .synapse/bin/activate pip install synapse matrix-synapse - curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit -# curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit + curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \ + | sed s/127.0.0.1/0.0.0.0/g | bash - name: Run integration tests on API ${{ matrix.api-level }} uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 8e21828562..0edd8c52df 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -184,18 +184,73 @@ class CommonTestHelper(context: Context) { /** * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(room: Room, message: String, count: Int) { + private fun sendTextMessagesBatched(room: Room, message: String, count: Int, rootThreadEventId: String? = null) { (1 until count + 1) .map { "$message #$it" } .chunked(10) .forEach { batchedMessages -> batchedMessages.forEach { formattedMessage -> - room.sendTextMessage(formattedMessage) + if (rootThreadEventId != null) { + room.replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = formattedMessage) + } else { + room.sendTextMessage(formattedMessage) + } } Thread.sleep(1_000L) } } + /** + * Reply in a thread + * @param room the room where to send the messages + * @param message the message to send + * @param numberOfMessages the number of time the message will be sent + */ + fun replyInThreadMessage( + room: Room, + message: String, + numberOfMessages: Int, + rootThreadEventId: String, + timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> { + + val sentEvents = ArrayList<TimelineEvent>(numberOfMessages) + val timeline = room.createTimeline(null, TimelineSettings(10)) + timeline.start() + waitWithLatch(timeout + 1_000L * numberOfMessages) { latch -> + val timelineListener = object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + } + + override fun onNewTimelineEvents(eventIds: List<String>) { + // noop + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + val newMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true } + + Timber.v("New synced message size: ${newMessages.size}") + if (newMessages.size == numberOfMessages) { + sentEvents.addAll(newMessages) + // Remove listener now, if not at the next update sendEvents could change + timeline.removeListener(this) + latch.countDown() + } + } + } + timeline.addListener(timelineListener) + sendTextMessagesBatched(room, message, numberOfMessages, rootThreadEventId) + } + timeline.dispose() + // Check that all events has been created + assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong()) + return sentEvents + } + // PRIVATE METHODS ***************************************************************************** /** diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt new file mode 100644 index 0000000000..79f5e8314d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.threads + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId +import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.events.model.isThread +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class GenerateThreadMessageTests : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + private val logPrefix = "---Test--> " + +// @Rule +// @JvmField +// val mRetryTestRule = RetryTestRule() + + @Test + fun reply_in_thread_to_normal_timeline_message_should_create_a_thread() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + val aliceRoom = aliceSession.getRoom(aliceRoomId)!! + + // Let's send a message in the normal timeline + val textMessage = "This is a normal timeline message" + val sentMessages = commonTestHelper.sendTextMessage( + room = aliceRoom, + message = textMessage, + nbOfMessages = 1) + + val initMessage = sentMessages.first() + + initMessage.root.isThread().shouldBeFalse() + initMessage.root.isTextMessage().shouldBeTrue() + initMessage.root.getRootThreadEventId().shouldBeNull() + initMessage.root.threadDetails?.isRootThread?.shouldBeFalse() + + // Let's reply in timeline to that message + val repliesInThread = commonTestHelper.replyInThreadMessage( + room = aliceRoom, + message = "Reply In the above thread", + numberOfMessages = 1, + rootThreadEventId = initMessage.root.eventId.orEmpty()) + + val replyInThread = repliesInThread.first() + replyInThread.root.isThread().shouldBeTrue() + replyInThread.root.isTextMessage().shouldBeTrue() + replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId) + + // The init normal message should now be a root thread event + val timeline = aliceRoom.createTimeline(null, TimelineSettings(30)) + timeline.start() + + aliceSession.startSync(true) + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails + initMessageThreadDetails?.isRootThread?.shouldBeTrue() ?: assert(false) + initMessageThreadDetails?.numberOfThreads?.shouldBe(1) + Timber.e("$logPrefix $initMessageThreadDetails") + true + } + timeline.addListener(eventsListener) + commonTestHelper.await(lock, 600_000) + } + aliceSession.stopSync() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt new file mode 100644 index 0000000000..099491655f --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.session.room.threads + +import android.util.Log +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Retry test rule used to retry test that failed. + * Retry failed test 3 times + */ +class RetryTestRule(val retryCount: Int = 3) : TestRule { + + private val TAG = RetryTestRule::class.java.simpleName + + override fun apply(base: Statement, description: Description): Statement { + return statement(base, description) + } + + private fun statement(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + var caughtThrowable: Throwable? = null + + // implement retry logic here + for (i in 0 until retryCount) { + try { + base.evaluate() + return + } catch (t: Throwable) { + caughtThrowable = t + Log.e(TAG, description.displayName + ": run " + (i + 1) + " failed") + } + } + + Log.e(TAG, description.displayName + ": giving up after " + retryCount + " failures") + throw caughtThrowable!! + } + } + } +}