Add github action workflow (#1016)

* Add build steps for github action

* Fix build commands

* Checkout submodule recursively and don't fail fast

* Fix mention view model tests

* Upload test result when failed

* Temporarily disable variants

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Remove deprecated properties

* Fixes up artifact uploading

* Fixes tests

* Fixes tests

* Huawei artifact matching and gradle caching

* PR trigger
pull/1712/head
SessionHero01 3 weeks ago committed by GitHub
parent e95fa6cc03
commit 9575db64fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,63 @@
name: Build and test
on:
push:
branches: [ "dev", "master" ]
pull_request:
types: [synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_test:
runs-on: ubuntu-latest
strategy:
fail-fast: false # Continue with other matrix items if one fails
matrix:
variant: [ 'play', 'website', 'huawei' ]
build_type: [ 'debug' ]
include:
- variant: 'huawei'
extra_build_command_options: '-Phuawei=1'
steps:
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '17'
- name: Build and test with Gradle
id: build
run: ./gradlew assemble${{ matrix.variant }}${{ matrix.build_type }} test${{ matrix.variant }}${{ matrix.build_type }}UnitTest ${{ matrix.extra_build_command_options }}
- name: Upload build reports regardless
if: always()
uses: actions/upload-artifact@v4
with:
name: build-reports-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/reports
if-no-files-found: ignore
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: session-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/outputs/apk/${{ matrix.variant }}/${{ matrix.build_type }}/*-universal*apk
if-no-files-found: error
compression-level: 0

@ -141,10 +141,7 @@ class MentionViewModel(
}
val myId = if (openGroup != null) {
AccountId(IdPrefix.BLINDED,
SodiumUtilities.blindedKeyPair(openGroup.publicKey,
requireNotNull(storage.getUserED25519KeyPair()))!!.publicKey.asBytes)
.hexString
requireNotNull(storage.getUserBlindedAccountId(openGroup.publicKey)).hexString
} else {
requireNotNull(storage.getUserPublicKey())
}

@ -228,6 +228,14 @@ open class Storage @Inject constructor(
override fun getUserED25519KeyPair(): KeyPair? { return KeyPairUtilities.getUserED25519KeyPair(context) }
override fun getUserBlindedAccountId(serverPublicKey: String): AccountId? {
val userKeyPair = getUserED25519KeyPair() ?: return null
return AccountId(
IdPrefix.BLINDED,
SodiumUtilities.blindedKeyPair(serverPublicKey, userKeyPair)!!.publicKey.asBytes
)
}
override fun getUserProfile(): Profile {
val displayName = usernameUtils.getCurrentUsername()
val profileKey = ProfileKeyUtil.getProfileKey(context)

@ -1,15 +1,11 @@
package org.thoughtcrime.securesms
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Rule
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
open class BaseCoroutineTest {
@get:Rule
var coroutinesTestRule = CoroutineTestRule()
protected fun runBlockingTest(test: suspend TestCoroutineScope.() -> Unit) =
coroutinesTestRule.runBlockingTest { test() }
protected fun runBlockingTest(test: suspend TestScope.() -> Unit) = runTest {
test()
}
}

@ -1,50 +0,0 @@
package org.thoughtcrime.securesms
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A
* [TestCoroutineScope] provides control over the execution of coroutines.
*
* Declare it as a JUnit Rule:
*
* ```
* @get:Rule
* var coroutineTestRule = CoroutineTestRule()
* ```
*
* Use it directly as a [TestCoroutineScope]:
*
* ```
* coroutineTestRule.pauseDispatcher()
* ...
* coroutineTestRule.resumeDispatcher()
* ...
* coroutineTestRule.runBlockingTest { }
* ...
*
*/
class CoroutineTestRule(
val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}

@ -2,24 +2,33 @@ package org.thoughtcrime.securesms
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.util.concurrent.atomic.AtomicBoolean
@ExperimentalCoroutinesApi
class MainCoroutineRule(private val dispatcher: TestDispatcher = StandardTestDispatcher()) :
@OptIn(ExperimentalCoroutinesApi::class)
class MainCoroutineRule() :
TestWatcher() {
companion object {
private val dispatcherSet = AtomicBoolean(false)
}
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
// Set the main dispatcher to test dispatcher, however we shouldn't reset the main dispatcher
// as some coroutine tasks spawn during the testing may try to resume on the main dispatcher,
// which will cause an exception if it has been reset.
// Right now there aren't good ways to force the coroutines run on other threads to behave
// correctly everytime so we'll just keep the main dispatcher as the test dispatcher globally.
if (dispatcherSet.compareAndSet(false, true)) {
Dispatchers.setMain(UnconfinedTestDispatcher(TestCoroutineScheduler()))
}
}
}

@ -23,6 +23,8 @@ import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.BaseCoroutineTest
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.ExpiryRadioOption
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCardData
@ -44,9 +46,8 @@ private val GROUP_ADDRESS = Address.fromSerialized(GROUP_NUMBER)
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(MockitoJUnitRunner::class)
class DisappearingMessagesViewModelTest {
class DisappearingMessagesViewModelTest : BaseViewModelTest() {
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import org.hamcrest.CoreMatchers.equalTo
@ -9,21 +10,29 @@ import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.time.ZonedDateTime
class ConversationViewModelTest: BaseViewModelTest() {
@get:Rule
val rule = MainCoroutineRule()
private val repository = mock<ConversationRepository>()
private val storage = mock<Storage>()
private val application = mock<Application>()
@ -49,7 +58,10 @@ class ConversationViewModelTest: BaseViewModelTest() {
configFactory = mock(),
groupManagerV2 = mock(),
callManager = mock(),
legacyGroupDeprecationManager = mock(),
legacyGroupDeprecationManager = mock {
on { deprecationState } doReturn MutableStateFlow(LegacyGroupDeprecationManager.DeprecationState.DEPRECATED)
on { deprecatedTime } doReturn MutableStateFlow(ZonedDateTime.now())
},
expiredGroupManager = mock(),
usernameUtils = mock()
)
@ -66,7 +78,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `should save draft message`() {
fun `should save draft message`() = runBlockingTest {
val draft = "Hi there"
viewModel.saveDraft(draft)
@ -76,7 +88,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `should retrieve draft message`() {
fun `should retrieve draft message`() = runBlockingTest {
val draft = "Hi there"
whenever(repository.getDraft(anyLong())).thenReturn(draft)
@ -87,7 +99,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `should invite contacts`() {
fun `should invite contacts`() = runBlockingTest {
val contacts = listOf<Recipient>()
viewModel.inviteContacts(contacts)
@ -96,7 +108,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `should unblock contact recipient`() {
fun `should unblock contact recipient`() = runBlockingTest {
whenever(recipient.isContactRecipient).thenReturn(true)
viewModel.unblock()
@ -167,7 +179,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `open group recipient should have no blinded recipient`() {
fun `open group recipient should have no blinded recipient`() = runBlockingTest {
whenever(recipient.isCommunityRecipient).thenReturn(true)
whenever(recipient.isCommunityOutboxRecipient).thenReturn(false)
whenever(recipient.isCommunityInboxRecipient).thenReturn(false)
@ -175,14 +187,14 @@ class ConversationViewModelTest: BaseViewModelTest() {
}
@Test
fun `local recipient should have input and no blinded recipient`() {
fun `local recipient should have input and no blinded recipient`() = runBlockingTest {
whenever(recipient.isLocalNumber).thenReturn(true)
assertThat(viewModel.shouldHideInputBar(), equalTo(false))
assertThat(viewModel.blindedRecipient, nullValue())
}
@Test
fun `contact recipient should hide input bar if not accepting requests`() {
fun `contact recipient should hide input bar if not accepting requests`() = runBlockingTest {
whenever(recipient.isCommunityInboxRecipient).thenReturn(true)
val blinded = mock<Recipient> {
whenever(it.blocksCommunityMessageRequests).thenReturn(true)

@ -21,11 +21,13 @@ import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
@RunWith(RobolectricTestRunner::class)
class MentionViewModelTest {
class MentionViewModelTest : BaseViewModelTest() {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val mainCoroutineRule = MainCoroutineRule()
@ -37,16 +39,22 @@ class MentionViewModelTest {
private data class MemberInfo(
val name: String,
val pubKey: String,
val roles: List<GroupMemberRole>
val roles: List<GroupMemberRole>,
val isMe: Boolean
)
private val myId = AccountId.fromString(
"151234567890123456789012345678901234567890123456789012345678901234"
)!!
private val threadMembers = listOf(
MemberInfo("Alice", "pubkey1", listOf(GroupMemberRole.ADMIN)),
MemberInfo("Bob", "pubkey2", listOf(GroupMemberRole.STANDARD)),
MemberInfo("Charlie", "pubkey3", listOf(GroupMemberRole.MODERATOR)),
MemberInfo("David", "pubkey4", listOf(GroupMemberRole.HIDDEN_ADMIN)),
MemberInfo("Eve", "pubkey5", listOf(GroupMemberRole.HIDDEN_MODERATOR)),
MemberInfo("李云海", "pubkey6", listOf(GroupMemberRole.ZOOMBIE)),
MemberInfo("You", myId.hexString, listOf(GroupMemberRole.STANDARD), isMe = true),
MemberInfo("Alice", "pubkey1", listOf(GroupMemberRole.ADMIN), isMe = false),
MemberInfo("Bob", "pubkey2", listOf(GroupMemberRole.STANDARD), isMe = false),
MemberInfo("Charlie", "pubkey3", listOf(GroupMemberRole.MODERATOR), isMe = false),
MemberInfo("David", "pubkey4", listOf(GroupMemberRole.HIDDEN_ADMIN), isMe = false),
MemberInfo("Eve", "pubkey5", listOf(GroupMemberRole.HIDDEN_MODERATOR), isMe = false),
MemberInfo("李云海", "pubkey6", listOf(GroupMemberRole.ZOOMBIE), isMe = false),
)
private val memberContacts = threadMembers.map { m ->
@ -106,7 +114,8 @@ class MentionViewModelTest {
},
storage = mock {
on { getOpenGroup(threadID) } doReturn openGroup
},
on { getUserBlindedAccountId(any()) } doReturn myId
},
dispatcher = StandardTestDispatcher(),
configFactory = mock(),
threadID = threadID,
@ -135,11 +144,11 @@ class MentionViewModelTest {
result as MentionViewModel.AutoCompleteState.Result
assertThat(result.members).isEqualTo(threadMembers.mapIndexed { index, m ->
val name =
val name = if (m.isMe) "You" else
memberContacts[index].displayName(Contact.ContactContext.OPEN_GROUP)
MentionViewModel.Candidate(
MentionViewModel.Member(m.pubKey, name, m.roles.any { it.isModerator }, isMe = false),
MentionViewModel.Member(m.pubKey, name, m.roles.any { it.isModerator }, isMe = m.isMe),
name,
0
)
@ -157,8 +166,8 @@ class MentionViewModelTest {
.isInstanceOf(MentionViewModel.AutoCompleteState.Result::class.java)
result as MentionViewModel.AutoCompleteState.Result
assertThat(result.members[0].member.name).isEqualTo("Alice (pubk...key1)")
assertThat(result.members[1].member.name).isEqualTo("Charlie (pubk...key3)")
assertThat(result.members[0].member.name).isEqualTo("Alice (pubkey1)")
assertThat(result.members[1].member.name).isEqualTo("Charlie (pubkey3)")
}
}
}

@ -1,14 +1,19 @@
package org.thoughtcrime.securesms.messagerequests
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.MainCoroutineRule
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
class MessageRequestsViewModelTest : BaseViewModelTest() {
@get:Rule
val rule = MainCoroutineRule()
private val repository = mock(ConversationRepository::class.java)
private val viewModel: MessageRequestsViewModel by lazy {

@ -22,7 +22,7 @@ navVersion=2.8.0-beta05
android.useAndroidX=true
appcompatVersion=1.6.1
coreVersion=1.13.1
coroutinesVersion=1.6.4
coroutinesVersion=1.9.0
curve25519Version=0.6.0
jetpackHiltVersion=1.2.0
daggerHiltVersion=2.55
@ -42,6 +42,8 @@ preferenceVersion=1.2.0
protobufVersion=4.29.3
testCoreVersion=1.5.0
zxingVersion=3.5.3
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
# Enable fast service loader to fix a crash in coroutine's test dispatcher set up
kotlinx.coroutines.fast.service.loader=true

@ -46,6 +46,7 @@ interface StorageProtocol {
fun getUserPublicKey(): String?
fun getUserED25519KeyPair(): KeyPair?
fun getUserX25519KeyPair(): ECKeyPair
fun getUserBlindedAccountId(serverPublicKey: String): AccountId?
fun getUserProfile(): Profile
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)

@ -4,8 +4,6 @@ package org.session.libsession.snode
import android.os.SystemClock
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.NullNode
import com.fasterxml.jackson.databind.node.TextNode
import com.goterl.lazysodium.exceptions.SodiumException
import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.PwHash
@ -18,6 +16,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all

@ -17,6 +17,11 @@ android {
kotlinOptions {
jvmTarget = '11'
}
buildFeatures {
buildConfig = true
}
namespace 'org.session.libsignal'
}

Loading…
Cancel
Save