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]' # Default to running both if not specified by caller test_flavors: description: 'Which flavors to test: "google", "fdroid", or "both"' required: false type: string default: 'both' secrets: GRADLE_ENCRYPTION_KEY: required: false CODECOV_TOKEN: required: true jobs: androidTest: runs-on: ubuntu-latest timeout-minutes: 45 strategy: matrix: api-level: ${{ fromJson(inputs.api_levels) }} # Use the input to define the matrix steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: 1 # Shallow clone - no version code calculation needed - name: Set up JDK 21 uses: actions/setup-java@v5 with: java-version: '21' 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 [ "${{ inputs.test_flavors }}" = "google" ]; then echo "tasks=connectedGoogleDebugAndroidTest" >> $GITHUB_OUTPUT elif [ "${{ inputs.test_flavors }}" = "fdroid" ]; then echo "tasks=connectedFdroidDebugAndroidTest" >> $GITHUB_OUTPUT else echo "tasks=connectedFdroidDebugAndroidTest connectedGoogleDebugAndroidTest" >> $GITHUB_OUTPUT fi - name: Run Android Instrumented Tests and Generate Coverage 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 --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 }} path: | **/build/outputs/androidTest-results/connected/** **/build/reports/androidTests/connected/** retention-days: 14