Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add instrumented test #44

Merged
merged 2 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2
max_line_length = 100
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

[*.{kt,kts}]
indent_size = 4
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
107 changes: 85 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,93 @@ on:
branches: [ "main" ]

jobs:
build:
checks:
name: Checks
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- run: chmod +x gradlew
- uses: gradle/gradle-build-action@v2
- run: ./gradlew lintKotlin
- run: ./gradlew lint

- name: Check with Gradle
run: ./gradlew check
unit-tests:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- run: chmod +x gradlew
- uses: gradle/gradle-build-action@v2
- run: ./gradlew testDebugUnitTest
- run: ./gradlew jacocoTestReport
- uses: actions/upload-artifact@v3
with:
name: unit-test-artifact
path: yorkie/build/jacoco/jacoco.xml

- name: Run JaCoCo
run: ./gradlew jacocoTestReport
instrumentation-tests:
name: Instrumentation tests
strategy:
matrix:
api-level: [ 23, 26, 28 ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- run: chmod +x gradlew
- uses: gradle/gradle-build-action@v2
- run: docker-compose -f docker/docker-compose-ci.yml up --build -d
- uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}-x86_64
- if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: adb uninstall dev.yorkie.test; ./gradlew createDebugAndroidTestCoverageReport
- uses: actions/upload-artifact@v3
with:
name: android-test-artifact
path: yorkie/build/reports/coverage/androidTest/debug/connected/report.xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
verbose: true
codecov:
name: Codecov
needs: [ unit-tests, instrumentation-tests ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v2
- uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
verbose: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ proguard/
captures/
*.iws
*.ipr
/.idea/androidTestResultsUserPreferences.xml

This file was deleted.

1 change: 1 addition & 0 deletions yorkie/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ dependencies {

androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation(kotlin("test"))
}

This file was deleted.

175 changes: 175 additions & 0 deletions yorkie/src/androidTest/kotlin/dev/yorkie/core/ClientTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package dev.yorkie.core

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dev.yorkie.core.Client.DocumentSyncResult
import dev.yorkie.core.Client.Event.DocumentChanged
import dev.yorkie.core.Client.Event.DocumentSynced
import dev.yorkie.core.Client.StreamConnectionStatus
import dev.yorkie.document.Document
import dev.yorkie.document.Document.Event.LocalChange
import dev.yorkie.document.Document.Event.RemoteChange
import dev.yorkie.document.crdt.CrdtPrimitive
import dev.yorkie.document.json.JsonPrimitive
import dev.yorkie.document.operation.RemoveOperation
import dev.yorkie.document.operation.SetOperation
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class ClientTest {

@Test
fun test_multiple_clients_working_on_same_document() {
runBlocking {
val client1 = createClient()
val client2 = createClient()
val documentKey = UUID.randomUUID().toString()
val document1 = createDocument(documentKey)
val document2 = createDocument(documentKey)

val client1Events = mutableListOf<Client.Event>()
val client2Events = mutableListOf<Client.Event>()
val document1Events = mutableListOf<Document.Event>()
val document2Events = mutableListOf<Document.Event>()
val collectJobs = listOf(
launch(start = CoroutineStart.UNDISPATCHED) {
client1.collect(client1Events::add)
},
launch(start = CoroutineStart.UNDISPATCHED) {
client2.collect(client2Events::add)
},
launch(start = CoroutineStart.UNDISPATCHED) {
document1.collect(document1Events::add)
},
launch(start = CoroutineStart.UNDISPATCHED) {
document2.collect(document2Events::add)
},
)

client1.activateAsync().await()
client2.activateAsync().await()

assertIs<Client.Status.Activated>(client1.status.value)
assertIs<Client.Status.Activated>(client2.status.value)

client1.attachAsync(document1).await()
var peerStatus = client1.peerStatus.dropWhile { it.isEmpty() }.first()
assertEquals(1, peerStatus.size)
assertEquals(peerStatus.first().actorId, client1.requireClientId())

client2.attachAsync(document2).await()
peerStatus = client1.peerStatus.dropWhile { it == peerStatus }.first()
assertEquals(2, peerStatus.size)
assertEquals(peerStatus.first().actorId, client1.requireClientId())
assertEquals(peerStatus.last().actorId, client2.requireClientId())

withTimeout(1_000) {
client1.streamConnectionStatus.first { it == StreamConnectionStatus.Connected }
client2.streamConnectionStatus.first { it == StreamConnectionStatus.Connected }
}

document1.updateAsync {
it["k1"] = "v1"
}.await()

while (client2Events.none { it is DocumentSynced }) {
delay(50)
}
val changeEvent = assertIs<DocumentChanged>(
client2Events.first { it is DocumentChanged },
)
assertContentEquals(listOf(Document.Key(documentKey)), changeEvent.documentKeys)
var syncEvent = assertIs<DocumentSynced>(client2Events.first { it is DocumentSynced })
assertIs<DocumentSyncResult.Synced>(syncEvent.result)

val localSetEvent = assertIs<LocalChange>(document1Events.first())
val localSetOperation = assertIs<SetOperation>(
localSetEvent.changeInfos.first().change.operations.first(),
)
assertEquals("k1", localSetOperation.key)
assertEquals("v1", (localSetOperation.value as CrdtPrimitive).value)
assertEquals(".k.1", localSetEvent.changeInfos.first().paths.first())
document1Events.clear()

val remoteSetEvent = assertIs<RemoteChange>(document2Events.first())
val remoteSetOperation = assertIs<SetOperation>(
remoteSetEvent.changeInfos.first().change.operations.first(),
)
assertEquals("k1", remoteSetOperation.key)
assertEquals("v1", (remoteSetOperation.value as CrdtPrimitive).value)
document2Events.clear()

val root2 = document2.getRoot()
assertEquals("v1", root2.getAs<JsonPrimitive>("k1").value)

client1Events.clear()
client2Events.clear()

document2.updateAsync {
it.remove("k1")
}.await()

while (client1Events.none { it is DocumentSynced }) {
delay(50)
}
syncEvent = assertIs(client2Events.first { it is DocumentSynced })
assertIs<DocumentSyncResult.Synced>(syncEvent.result)
val root1 = document1.getRoot()
assertTrue(root1.keys.isEmpty())

val remoteRemoveEvent = assertIs<RemoteChange>(document1Events.first())
val remoteRemoveOperation = assertIs<RemoveOperation>(
remoteRemoveEvent.changeInfos.first().change.operations.first(),
)
assertEquals(localSetOperation.effectedCreatedAt, remoteRemoveOperation.createdAt)

val localRemoveEvent = assertIs<LocalChange>(document2Events.first())
val localRemoveOperation = assertIs<RemoveOperation>(
localRemoveEvent.changeInfos.first().change.operations.first(),
)
assertEquals(remoteSetOperation.effectedCreatedAt, localRemoveOperation.createdAt)

assertEquals(1, document1.clone?.getGarbageLength())
assertEquals(1, document2.clone?.getGarbageLength())

client1.updatePresenceAsync("k2", "v2").await()
peerStatus = client2.peerStatus.dropWhile { it == peerStatus }.first()
val status = peerStatus.first { it.actorId == client1.requireClientId() }
assertEquals(mapOf("k2" to "v2"), status.presenceInfo.data)
assertTrue(
peerStatus.first {
it.actorId == client2.requireClientId()
}.presenceInfo.data.isEmpty(),
)

client1.detachAsync(document1).await()
client2.detachAsync(document2).await()
client1.deactivateAsync().await()
client2.deactivateAsync().await()

collectJobs.forEach(Job::cancel)
}
}

private fun createClient() = Client(
InstrumentationRegistry.getInstrumentation().targetContext,
"10.0.2.2:8080",
true,
)

private fun createDocument(key: String) = Document(Document.Key(key))
}
Loading