From b35505ff59fd98510e25cb618bfb3d523bd647f6 Mon Sep 17 00:00:00 2001 From: Hugo Melder Date: Tue, 9 Jul 2024 18:59:33 +0200 Subject: [PATCH] Android CI with Unit Testing (#295) * Add android CI scripts * Android Test Driver: Busybox compatibility --- .github/scripts/android_test_driver.sh | 61 +++++++++++++++++++++ .github/scripts/android_test_main.sh | 63 ++++++++++++++++++++++ .github/workflows/main.yml | 73 ++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 .github/scripts/android_test_driver.sh create mode 100755 .github/scripts/android_test_main.sh diff --git a/.github/scripts/android_test_driver.sh b/.github/scripts/android_test_driver.sh new file mode 100644 index 0000000..88d263f --- /dev/null +++ b/.github/scripts/android_test_driver.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +# This script is run on the emulator to run the tests + +# Get list of all binaries in pwd for later iteration (only include binaries) +BINARIES=$(find . -maxdepth 1 -type f ! -name "*.so" ! -name "android_test_driver.sh") + +TOTAL=0 +PASS=0 +SKIP=0 +FAIL=0 + +# Run each binary, measure time and return value (PASS or FAIL). Print stdout and stderr only after a failure +for BINARY in $BINARIES; do + TOTAL=$((TOTAL + 1)) + + START_TIME=$(date +%s) + START_TIME_MS=$((START_TIME * 1000 + $(date +%N) / 1000000)) + + OUTPUT=$("$BINARY" 2>&1) + EXIT_CODE=$? + + END_TIME=$(date +%s) + END_TIME_MS=$((END_TIME * 1000 + $(date +%N) / 1000000)) + ELAPSED_TIME=$((END_TIME_MS - START_TIME_MS)) + + BINARY_NAME=$(basename "$BINARY") + + if [ $EXIT_CODE -eq 0 ]; then + PASS=$((PASS + 1)) + echo "PASSED ($EXIT_CODE): $BINARY_NAME (${ELAPSED_TIME}ms)" + elif [ $EXIT_CODE -eq 77 ]; then + SKIP=$((SKIP + 1)) + echo "SKIPPED: $BINARY_NAME" + else + FAIL=$((FAIL + 1)) + echo "FAILED ($EXIT_CODE): $BINARY_NAME (${ELAPSED_TIME}ms)" + if [ -z "$OUTPUT" ]; then + echo "No output written to stdout." + else + echo "Output:" + echo "$OUTPUT" + fi + fi +done + +if [ $TOTAL -eq 0 ]; then + echo "No tests found. Exiting." + exit 1 +fi + +PERCENTAGE=$(((PASS + SKIP) * 100 / TOTAL)) +echo "$PERCENTAGE% Passed. Total: $TOTAL, Passed: $PASS, Skipped: $SKIP, Failed: $FAIL" +echo "Finished running tests. Exiting." + +# Exit with corresponding return value +if [ $FAIL -eq 0 ]; then + exit 0 +else + exit 1 +fi diff --git a/.github/scripts/android_test_main.sh b/.github/scripts/android_test_main.sh new file mode 100755 index 0000000..29abcbe --- /dev/null +++ b/.github/scripts/android_test_main.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +main () { + # first argument is the build directory + local BUILD_DIR=$1 + # second argument is the android ndk sysroot + local ANDROID_NDK_SYSROOT=$2 + # third argument is the target triple + # e.g. arm-linux-androideabi, aarch64-linux-android, x86_64-linux-android + local TARGET_TRIPLE=$3 + + if [ ! -d "$BUILD_DIR" ] + then + echo "Build directory argument not found" + exit 1 + fi + if [ ! -d "$ANDROID_NDK_SYSROOT" ] + then + echo "Android NDK sysroot argument not found" + exit 1 + fi + if [ -z "$TARGET_TRIPLE" ] + then + echo "Target triple argument not found" + exit 1 + fi + + # We need to run the emulator with root permissions + # This is needed to run the tests + adb root + + local TEMP_DIR=$(mktemp -d) + + # Copy libobjc.so and test binaries to temporary directory + cp $BUILD_DIR/libobjc.so* $TEMP_DIR + cp $BUILD_DIR/Test/* $TEMP_DIR + + for file in $TEMP_DIR/*; do + # Check if file is a binary + if ! file $file | grep -q "ELF" + then + rm $file + continue + fi + + # Set runtime path to ORIGIN + patchelf --set-rpath '$ORIGIN' $file + done + + # Copy libc++_shared.so (required by libobjc2) + cp $ANDROID_NDK_SYSROOT/usr/lib/$TARGET_TRIPLE/libc++_shared.so $TEMP_DIR + + adb shell rm -rf /data/local/tmp/libobjc2_tests + adb push $TEMP_DIR /data/local/tmp/libobjc2_tests + + # Copy android_test_driver.sh to device + adb push $BUILD_DIR/../.github/scripts/android_test_driver.sh /data/local/tmp/libobjc2_tests + + # Run the tests + adb shell "cd /data/local/tmp/libobjc2_tests && sh android_test_driver.sh" +} + +main "$@" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 413f72b..0f38959 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -259,6 +259,79 @@ jobs: name: ${{ matrix.msystem }}-${{ matrix.build-type }} path: dist/ + android: + strategy: + matrix: + # Build each combination of OS and release/debug variants + os: [ ubuntu-20.04 ] + build-type: [ Release, Debug ] + arch: + - name: x86_64 + triple: x86_64-linux-android + api-level: [ 26 ] + # Don't abort runners if a single one fails + fail-fast: false + runs-on: ${{ matrix.os }} + name: Android ${{ matrix.build-type }} ${{ matrix.arch.name }} API-${{ matrix.api-level }} + steps: + - uses: actions/checkout@v4 + - name: Install Dependencies + run: | + sudo apt-get update -y + sudo apt-get install patchelf ninja-build -y + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r26d + - name: Configure CMake + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64 + export CCPREFIX=$TOOLCHAIN/bin/${{ matrix.arch.triple }}${{ matrix.api-level }} + export CC="$CCPREFIX-clang" + export CXX="$CCPREFIX-clang++" + export OBJC="$CCPREFIX-clang" + export OBJCXX="$CCPREFIX-clang++" + export AS="$CCPREFIX-clang" + export LD="$TOOLCHAIN/bin/ld.lld" + export AR="$TOOLCHAIN/bin/llvm-ar" + export RANLIB="$TOOLCHAIN/bin/llvm-ranlib" + export STRIP="$TOOLCHAIN/bin/llvm-strip" + export NM="$TOOLCHAIN/bin/llvm-nm" + export OBJDUMP="$TOOLCHAIN/bin/llvm-objdump" + export LDFLAGS="-fuse-ld=lld" + export LIBS="-lc++_shared" + + cmake -B ${{github.workspace}}/build \ + -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=${{ matrix.arch.name }} \ + -DANDROID_NDK=$ANDROID_NDK_HOME \ + -DANDROID_STL=c++_shared \ + -DCMAKE_FIND_USE_CMAKE_PATH=false \ + -DCMAKE_C_COMPILER=$CC \ + -DCMAKE_CXX_COMPILER=$CXX \ + -DCMAKE_ASM_COMPILER=$AS \ + -DCMAKE_BUILD_TYPE=${{matrix.build-type}} \ + -DTESTS=ON \ + -DANDROID_PLATFORM=android-${{ matrix.api-level }} \ + -G Ninja + - name: Build + working-directory: ${{github.workspace}}/build + run: | + NINJA_STATUS="%p [%f:%s/%t] %o/s, %es" ninja -v + - name: Test + uses: reactivecircus/android-emulator-runner@v2 + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + with: + api-level: ${{ matrix.api-level }} + target: default + arch: ${{ matrix.arch.name }} + script: | + ${{github.workspace}}/.github/scripts/android_test_main.sh ${{github.workspace}}/build ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot ${{ matrix.arch.triple }} + + # Fake check that can be used as a branch-protection rule. all-checks: needs: [ubuntu, windows, qemu-crossbuild]