From bd03bf45c28dde7d60fc8f723dfc7b9e8b6896ee Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 7 Feb 2024 13:19:57 +1100 Subject: [PATCH] Initial CI setup test Added code coverage report generation (for future CI stuff) Fixed a flaky unit test --- .drone.jsonnet | 70 +++++++++++++++++++ .gitignore | 5 +- app/build.gradle | 22 ++++++ .../v2/ConversationViewModelTest.kt | 5 +- scripts/drone-static-upload.sh | 70 +++++++++++++++++++ scripts/drone-upload-exists.sh | 58 +++++++++++++++ 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 .drone.jsonnet create mode 100755 scripts/drone-static-upload.sh create mode 100755 scripts/drone-upload-exists.sh diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 0000000000..2a6c55f8f8 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,70 @@ +// Intentionally doing a depth of 2 as libSession-util has it's own submodules (and libLokinet likely will as well) +local clone_submodules = { + name: 'Clone Submodules', + commands: ['git fetch --tags', 'git submodule update --init --recursive --depth=2'] +}; + +// cmake options for static deps mirror +local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else ''); + +[ + // Unit tests (PRs only) + { + kind: 'pipeline', + type: 'exec', + name: 'Unit Tests', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'push' ] } }, + steps: [ + clone_submodules, + { + name: 'Run Unit Tests', + image: 'registry.oxen.rocks/lokinet-ci-android', + commands: [ + './gradlew testPlayDebugUnitTestCoverageReport' + ], + } + ], + }, + // Validate build artifact was created by the direct branch push (PRs only) + { + kind: 'pipeline', + type: 'exec', + name: 'Check Build Artifact Existence', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'push' ] } }, + steps: [ + { + name: 'Poll for build artifact existence', + commands: [ + './Scripts/drone-upload-exists.sh' + ] + } + ] + }, + // Debug APK build (non-PRs only) + { + kind: 'pipeline', + type: 'exec', + name: 'Debug APK Build', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'pull_request' ] } }, + steps: [ + clone_submodules, + { + name: 'Build', + image: 'registry.oxen.rocks/lokinet-ci-android', + commands: [ + './gradlew assemblePlayDebug' + ], + }, + { + name: 'Upload artifacts', + environment: { SSH_KEY: { from_secret: 'SSH_KEY' } }, + commands: [ + './Scripts/drone-static-upload.sh' + ] + }, + ], + } +] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 023fc81010..be928b3933 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ ffpr *.sh pkcs11.password app/play -app/huawei \ No newline at end of file +app/huawei + +!/scripts/drone-static-upload.sh +!/scripts/drone-upload-exists.sh \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 70ed8efa38..5002756607 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -124,6 +124,7 @@ android { debug { isDefault true minifyEnabled false + enableUnitTestCoverage true } } @@ -201,6 +202,27 @@ android { } } } + + task testPlayDebugUnitTestCoverageReport(type: JacocoReport, dependsOn: "testPlayDebugUnitTest") { + reports { + xml.enabled = true + } + + // Add files that should not be listed in the report (e.g. generated Files from dagger) + def fileFilter = [] + def mainSrc = "$projectDir/src/main/java" + def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/playDebug", excludes: fileFilter) + + // Compiled Kotlin class files are written into build-variant-specific subdirectories of 'build/tmp/kotlin-classes'. + classDirectories.from = files([kotlinDebugTree]) + + // To produce an accurate report, the bytecode is mapped back to the original source code. + sourceDirectories.from = files([mainSrc]) + + // Execution data generated when running the tests against classes instrumented by the JaCoCo agent. + // This is enabled with 'enableUnitTestCoverage' in the 'debug' build type. + executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/playDebugUnitTest/testPlayDebugUnitTest.exec" + } } dependencies { diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index aecbd7e4a8..c3a3528f14 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -10,12 +10,14 @@ import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.Before import org.junit.Test +import org.mockito.Mockito import org.mockito.Mockito.anyLong import org.mockito.Mockito.anySet import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import org.mockito.verification.VerificationMode import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.database.Storage @@ -49,7 +51,8 @@ class ConversationViewModelTest: BaseViewModelTest() { viewModel.saveDraft(draft) - verify(repository).saveDraft(threadId, draft) + // The above is an async process to wait 100ms to give it a chance to complete + verify(repository, Mockito.timeout(100).times(1)).saveDraft(threadId, draft) } @Test diff --git a/scripts/drone-static-upload.sh b/scripts/drone-static-upload.sh new file mode 100755 index 0000000000..ffce9c077f --- /dev/null +++ b/scripts/drone-static-upload.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Script used with Drone CI to upload build artifacts (because specifying all this in +# .drone.jsonnet is too painful). + +set -o errexit + +if [ -z "$SSH_KEY" ]; then + echo -e "\n\n\n\e[31;1mUnable to upload artifact: SSH_KEY not set\e[0m" + # Just warn but don't fail, so that this doesn't trigger a build failure for untrusted builds + exit 0 +fi + +echo "$SSH_KEY" >ssh_key + +set -o xtrace # Don't start tracing until *after* we write the ssh key + +chmod 600 ssh_key + +# Define the output paths +build_dir="app/build/outputs/apk/play/debug" +target_path=$(ls ${build_dir} | grep -o 'session-[^[:space:]]*-universal.apk') + +# Validate the paths exist +if [ ! -d $build_path ]; then + echo -e "\n\n\n\e[31;1mExpected a file to upload, found none\e[0m" >&2 + exit 1 +fi + +if [ -n "$DRONE_TAG" ]; then + # For a tag build use something like `session-android-v1.2.3-universal` + base="session-android-$DRONE_TAG-universal" +else + # Otherwise build a length name from the datetime and commit hash, such as: + # session-android-20200522T212342Z-04d7dcc54-universal + base="session-android-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}-universal" +fi + +# Copy over the build products +mkdir -vp "$base" +cp -av $target_path "$base" + +# tar dat shiz up yo +archive="$base.tar.xz" +tar cJvf "$archive" "$base" + +upload_to="oxen.rocks/${DRONE_REPO// /_}/${DRONE_BRANCH// /_}" + +# sftp doesn't have any equivalent to mkdir -p, so we have to split the above up into a chain of +# -mkdir a/, -mkdir a/b/, -mkdir a/b/c/, ... commands. The leading `-` allows the command to fail +# without error. +upload_dirs=(${upload_to//\// }) +put_debug= +mkdirs= +dir_tmp="" +for p in "${upload_dirs[@]}"; do + dir_tmp="$dir_tmp$p/" + mkdirs="$mkdirs +-mkdir $dir_tmp" +done + +sftp -i ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks <