name: Reusable Android Instrumented Tests on: workflow_call: inputs: upload_artifacts: description: 'Whether to upload Android test reports' required: false type: boolean default: true api_levels: description: 'JSON array string of API levels to run tests on (e.g., `[35]` or `[26, 34, 35]`)' required: false type: string default: '[26, 35]' test_flavors: description: 'Which flavors to test: "google", "fdroid", or "both"' required: false type: string default: 'both' num_shards: description: 'Number of shards to split tests into' required: false type: number default: 1 secrets: GRADLE_ENCRYPTION_KEY: required: false CODECOV_TOKEN: required: true GRADLE_CACHE_URL: required: false GRADLE_CACHE_USERNAME: required: false GRADLE_CACHE_PASSWORD: required: false jobs: setup-matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - id: set-matrix run: | API_LEVELS='${{ inputs.api_levels }}' FLAVORS='${{ inputs.test_flavors }}' NUM_SHARDS=${{ inputs.num_shards }} if [ "$FLAVORS" = "both" ]; then FLAVORS_JSON='["google", "fdroid"]' else FLAVORS_JSON="[\"$FLAVORS\"]" fi SHARDS_JSON=$(seq 0 $((NUM_SHARDS - 1)) | jq -R . | jq -s -c .) echo "matrix={\"api_level\":$API_LEVELS,\"flavor\":$FLAVORS_JSON,\"shard\":$SHARDS_JSON}" >> $GITHUB_OUTPUT androidTest: needs: setup-matrix runs-on: ubuntu-latest timeout-minutes: 45 env: GRADLE_OPTS: "-Dorg.gradle.daemon=false" GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} strategy: fail-fast: false matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: 1 - name: Set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'jetbrains' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} build-scan-publish: true build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' build-scan-terms-of-use-agree: 'yes' add-job-summary: always - name: AVD cache uses: actions/cache@v5 id: avd-cache with: path: | ~/.android/avd/* ~/.android/adb* key: avd-${{ matrix.api_level }} - name: create AVD and generate snapshot for caching 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." - name: Determine test tasks id: test-tasks run: | if [ "${{ matrix.flavor }}" = "google" ]; then echo "tasks=connectedGoogleDebugAndroidTest" >> $GITHUB_OUTPUT else echo "tasks=connectedFdroidDebugAndroidTest" >> $GITHUB_OUTPUT fi - name: Run Sharded Android Instrumented Tests uses: reactivecircus/android-emulator-runner@v2 env: ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60 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: ./gradlew ${{ steps.test-tasks.outputs.tasks }} koverXmlReport -Pci=true -Pandroid.testInstrumentationRunnerArguments.numShards=${{ inputs.num_shards }} -Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }} --continue --scan && ( killall -INT crashpad_handler || true ) - name: Upload coverage reports to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: coverage slug: meshtastic/Meshtastic-Android files: "**/build/reports/kover/report.xml" - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results directory: . files: "**/build/test-results/**/*.xml,**/build/outputs/androidTest-results/**/*.xml" - name: Upload Test Results if: ${{ always() && inputs.upload_artifacts }} uses: actions/upload-artifact@v6 with: name: android-test-reports-api-${{ matrix.api_level }}-${{ matrix.flavor }}-shard-${{ matrix.shard }} path: | **/build/outputs/androidTest-results/connected/** **/build/reports/androidTests/connected/** retention-days: 14