Initial CI setup test

Added code coverage report generation (for future CI stuff)
Fixed a flaky unit test
This commit is contained in:
Morgan Pretty 2024-02-07 13:19:57 +11:00
parent 343820579c
commit bd03bf45c2
6 changed files with 228 additions and 2 deletions

70
.drone.jsonnet Normal file
View File

@ -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'
]
},
],
}
]

3
.gitignore vendored
View File

@ -17,3 +17,6 @@ ffpr
pkcs11.password pkcs11.password
app/play app/play
app/huawei app/huawei
!/scripts/drone-static-upload.sh
!/scripts/drone-upload-exists.sh

View File

@ -124,6 +124,7 @@ android {
debug { debug {
isDefault true isDefault true
minifyEnabled false 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 { dependencies {

View File

@ -10,12 +10,14 @@ import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet import org.mockito.Mockito.anySet
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.mockito.verification.VerificationMode
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
@ -49,7 +51,8 @@ class ConversationViewModelTest: BaseViewModelTest() {
viewModel.saveDraft(draft) 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 @Test

70
scripts/drone-static-upload.sh Executable file
View File

@ -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 <<SFTP
$mkdirs
put $archive $upload_to
$put_debug
SFTP
set +o xtrace
echo -e "\n\n\n\n\e[32;1mUploaded to https://${upload_to}/${archive}\e[0m\n\n\n"

58
scripts/drone-upload-exists.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
#
# Script used with Drone CI to check for the existence of a build artifact.
if [[ -z ${DRONE_REPO} || -z ${DRONE_PULL_REQUEST} ]]; then
echo -e "\n\n\n\n\e[31;1mRequired env variables not specified, likely a tag build so just failing\e[0m\n\n\n"
exit 1
fi
# This file info MUST match the structure of `base` in the `drone-static-upload.sh` script in
# order to function correctly
prefix="session-android-"
suffix="-${DRONE_COMMIT:0:9}-universal.tar.xz"
# Extracting head.label using string manipulation
echo "Extracting repo information for 'https://api.github.com/repos/${DRONE_REPO}/pulls/${DRONE_PULL_REQUEST}'"
pr_info=$(curl -s https://api.github.com/repos/${DRONE_REPO}/pulls/${DRONE_PULL_REQUEST})
pr_info_clean=$(echo "$pr_info" | tr -d '[:space:]')
head_info=$(echo "$pr_info_clean" | sed -n 's/.*"head"\(.*\)"base".*/\1/p')
fork_repo=$(echo "$head_info" | grep -o '"full_name":"[^"]*' | sed 's/"full_name":"//')
fork_branch=$(echo "$head_info" | grep -o '"ref":"[^"]*' | sed 's/"ref":"//')
upload_dir="https://oxen.rocks/${fork_repo}/${fork_branch}"
echo "Starting to poll ${upload_dir} every 10s to check for a build matching '${prefix}.*${suffix}'"
# Loop indefinitely the CI can timeout the script if it takes too long
total_poll_duration=0
max_poll_duration=$((30 * 60)) # Poll for a maximum of 30 mins
while true; do
# Need to add the trailing '/' or else we get a '301' response
build_artifacts_html=$(curl -s "${upload_dir}/")
if [ $? != 0 ]; then
echo "Failed to retrieve build artifact list"
exit 1
fi
# Extract 'session-ios...' titles using grep and awk
current_build_artifacts=$(echo "$build_artifacts_html" | grep -o 'href="${prefix}[^"]*' | sed 's/href="//')
# Use grep to check for the combination
target_file=$(echo "$current_build_artifacts" | grep -o "${prefix}.*${suffix}" | tail -n 1)
if [ -n "$target_file" ]; then
echo -e "\n\n\n\n\e[32;1mExisting build artifact at ${upload_dir}/${target_file}\e[0m\n\n\n"
exit 0
fi
# Sleep for 10 seconds before checking again
sleep 10
total_poll_duration=$((total_poll_duration + 10))
if [ $total_poll_duration -gt $max_poll_duration ]; then
echo -e "\n\n\n\n\e[31;1mCould not find existing build artifact after polling for 30 minutes\e[0m\n\n\n"
exit 1
fi
done