Merge pull request #515 from Eureka-ch/chore/unit-test-conventions #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI - Test Runner | |
| # Run the workflow when commits are pushed on main or when a PR is modified | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: | |
| - opened | |
| - synchronize | |
| - reopened | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| android: ${{ steps.filter.outputs.android }} | |
| functions: ${{ steps.filter.outputs.functions }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| android: | |
| - 'app/**' | |
| - 'gradle/**' | |
| - '**.gradle' | |
| - '**.gradle.kts' | |
| - '**.kt' | |
| - '**.xml' | |
| - '.github/workflows/ci.yml' | |
| functions: | |
| - 'functions/**' | |
| - 'firebase.json' | |
| ktfmt-check: | |
| name: KTFmt Check | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.android == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Android Build | |
| uses: ./.github/actions/setup-android-build | |
| - name: KTFmt Check | |
| run: ./gradlew ktfmtCheck | |
| build: | |
| name: Build & Lint | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.android == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Android Build | |
| uses: ./.github/actions/setup-android-build | |
| - name: Decode Secrets | |
| uses: ./.github/actions/decode-secrets | |
| env: | |
| GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} | |
| with: | |
| level: build | |
| - name: Build and Lint | |
| run: ./gradlew compileDebugKotlin compileDebugJavaWithJavac lint --parallel --build-cache | |
| - name: Upload build artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| path: | | |
| app/build/tmp/kotlin-classes/debug/** | |
| app/build/reports/lint-results-debug.xml | |
| compression-level: 9 | |
| unit-tests: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.android == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Android Build | |
| uses: ./.github/actions/setup-android-build | |
| - name: Decode Secrets | |
| uses: ./.github/actions/decode-secrets | |
| env: | |
| GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} | |
| FIRESTORE_RULES: ${{ secrets.FIRESTORE_RULES }} | |
| STORAGE_RULES: ${{ secrets.STORAGE_RULES }} | |
| GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }} | |
| with: | |
| level: full | |
| - name: Run unit tests | |
| run: ./gradlew check -x lint --parallel --build-cache --info --stacktrace | |
| - name: Upload test results and coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-test-results | |
| path: | | |
| app/build/test-results/**/*.xml | |
| app/build/outputs/unit_test_code_coverage/**/*.exec | |
| compression-level: 9 | |
| setup-avd: | |
| name: Setup AVD | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| if: needs.changes.outputs.android == 'true' || needs.changes.outputs.functions == 'true' | |
| steps: | |
| - 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: AVD cache | |
| uses: actions/cache@v4 | |
| id: avd-cache | |
| with: | |
| path: | | |
| ~/.android/avd/* | |
| ~/.android/adb* | |
| key: avd-36-google_apis-x86_64-pixel_7_pro | |
| - 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: 36 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_7_pro | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back emulated -camera-front emulated -memory 4096 -cores 4 | |
| disable-animations: true | |
| script: echo "AVD snapshot generated" | |
| build-functions: | |
| name: Build Cloud Functions | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.android == 'true' || needs.changes.outputs.functions == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Cache Cloud Functions dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: functions/node_modules | |
| key: ${{ runner.os }}-functions-${{ hashFiles('functions/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-functions- | |
| - name: Build Cloud Functions | |
| run: | | |
| cd functions | |
| npm install | |
| npm run build | |
| cd .. | |
| functions-tests: | |
| name: Cloud Functions Tests | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: needs.changes.outputs.functions == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Cache Cloud Functions dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: functions/node_modules | |
| key: ${{ runner.os }}-functions-${{ hashFiles('functions/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-functions- | |
| - name: Install dependencies | |
| run: | | |
| cd functions | |
| npm install | |
| - name: Run TypeScript tests with coverage | |
| run: | | |
| cd functions | |
| npm run test:coverage | |
| - name: Upload TypeScript coverage report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: functions-coverage-report | |
| path: functions/coverage/ | |
| compression-level: 9 | |
| instrumented-tests: | |
| name: Instrumented Tests (Shard ${{ matrix.shard }}) | |
| runs-on: ubuntu-latest | |
| needs: [changes, setup-avd, build-functions] | |
| if: needs.changes.outputs.android == 'true' || needs.changes.outputs.functions == 'true' | |
| # To modify shard count: | |
| # 1. Update numShards in gradle command (currently 8) | |
| # 2. Update matrix.shard array to [0, 1, ..., N-1] | |
| # 3. Update job name "Shard X/N" if desired | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2, 3, 4, 5, 6, 7] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - 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 Android Build | |
| uses: ./.github/actions/setup-android-build | |
| - name: AVD cache | |
| uses: actions/cache@v4 | |
| id: avd-cache | |
| with: | |
| path: | | |
| ~/.android/avd/* | |
| ~/.android/adb* | |
| key: avd-36-google_apis-x86_64-pixel_7_pro | |
| - 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: 36 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_7_pro | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back emulated -camera-front emulated -memory 4096 -cores 4 | |
| disable-animations: true | |
| script: echo "Generated AVD snapshot for caching." | |
| - name: Decode Secrets | |
| uses: ./.github/actions/decode-secrets | |
| env: | |
| GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} | |
| FIRESTORE_RULES: ${{ secrets.FIRESTORE_RULES }} | |
| STORAGE_RULES: ${{ secrets.STORAGE_RULES }} | |
| GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }} | |
| with: | |
| level: full | |
| - name: Setup NodeJS | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Cache Firebase CLI | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-firebase-cli-v1 | |
| restore-keys: | | |
| ${{ runner.os }}-firebase-cli- | |
| - name: Cache Firebase Emulators | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/firebase/emulators | |
| key: ${{ runner.os }}-firebase-emulators-v1 | |
| restore-keys: | | |
| ${{ runner.os }}-firebase-emulators- | |
| - name: Cache Cloud Functions dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: functions/node_modules | |
| key: ${{ runner.os }}-functions-${{ hashFiles('functions/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-functions- | |
| - name: Install Firebase CLI | |
| run: npm install -g firebase-tools | |
| - name: Setup JDK 21 for Firebase Emulators | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "21" | |
| - name: Build Cloud Functions | |
| run: | | |
| cd functions | |
| npm install | |
| npm run build | |
| - name: Start Firebase emulators | |
| run: | | |
| if [ -e "firebase.json" ] && jq -e '.emulators' firebase.json >/dev/null; then | |
| echo "Starting Firebase emulators for instrumentation tests..." | |
| firebase emulators:start --only storage,firestore,auth,functions --project eureka-app-ch & | |
| sleep 20 | |
| echo "Firebase emulators started" | |
| else | |
| echo "Firebase emulators not configured, skipping emulator startup..." | |
| fi | |
| - name: Restore JDK 17 for Android Tests | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "17" | |
| cache: gradle | |
| - name: Verify Sharding Configuration | |
| run: | | |
| echo "============================================" | |
| echo "Shard Configuration:" | |
| echo " numShards: 8" | |
| echo " shardIndex: ${{ matrix.shard }}" | |
| echo " Expected: ~50 tests per shard" | |
| echo "============================================" | |
| - name: run tests | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 36 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_7_pro | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back emulated -camera-front emulated -memory 4096 -cores 4 | |
| disable-animations: true | |
| script: ./gradlew connectedCheck -x lint --parallel --info --stacktrace -Pandroid.testInstrumentationRunnerArguments.numShards=8 -Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }} | |
| - name: Rename coverage file for shard | |
| if: always() | |
| run: | | |
| echo "=== Shard ${{ matrix.shard }}: Renaming coverage file ===" | |
| echo "Looking for coverage files in app/build/outputs/code_coverage..." | |
| find app/build/outputs/code_coverage -type f -name "*.ec" || echo "No .ec files found" | |
| echo "" | |
| # Find and rename the coverage.ec file to include shard number | |
| COVERAGE_FILE=$(find app/build/outputs/code_coverage -name "coverage.ec" -type f | head -1) | |
| if [ -n "$COVERAGE_FILE" ]; then | |
| echo "Found coverage file: $COVERAGE_FILE" | |
| COVERAGE_DIR=$(dirname "$COVERAGE_FILE") | |
| mv -v "$COVERAGE_FILE" "$COVERAGE_DIR/coverage-shard-${{ matrix.shard }}.ec" | |
| echo "✓ Renamed coverage file to coverage-shard-${{ matrix.shard }}.ec" | |
| ls -lh "$COVERAGE_DIR/coverage-shard-${{ matrix.shard }}.ec" | |
| else | |
| echo "✗ Warning: No coverage.ec file found for shard ${{ matrix.shard }}" | |
| echo "Contents of app/build/outputs/:" | |
| ls -R app/build/outputs/ || echo "Directory does not exist" | |
| fi | |
| - name: Upload instrumented test results and coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: instrumented-test-results-shard-${{ matrix.shard }} | |
| path: | | |
| app/build/outputs/androidTest-results/**/*.xml | |
| app/build/outputs/code_coverage/**/* | |
| compression-level: 9 | |
| reports: | |
| name: Coverage & SonarCloud | |
| runs-on: ubuntu-latest | |
| needs: [ktfmt-check, unit-tests, instrumented-tests, build] | |
| if: success() | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "17" | |
| cache: gradle | |
| - name: Decode secrets | |
| env: | |
| GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} | |
| FIRESTORE_RULES: ${{ secrets.FIRESTORE_RULES }} | |
| STORAGE_RULES: ${{ secrets.STORAGE_RULES }} | |
| GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }} | |
| run: | | |
| if [ -n "$GOOGLE_SERVICES" ]; then | |
| echo "$GOOGLE_SERVICES" | base64 --decode > ./app/google-services.json | |
| else | |
| echo "::warning::GOOGLE_SERVICES secret is not set. google-services.json will not be created. Should be present after B2" | |
| fi | |
| if [ -n "$FIRESTORE_RULES" ]; then | |
| mkdir -p ./firebase/firestore | |
| echo "$FIRESTORE_RULES" | base64 --decode > ./firebase/firestore/firestore.rules | |
| fi | |
| if [ -n "$STORAGE_RULES" ]; then | |
| mkdir -p ./firebase/storage | |
| echo "$STORAGE_RULES" | base64 --decode > ./firebase/storage/storage.rules | |
| fi | |
| if [ -n "$GOOGLE_CLOUD_CREDENTIALS" ]; then | |
| mkdir -p ./functions | |
| echo "$GOOGLE_CLOUD_CREDENTIALS" | base64 --decode > ./functions/eureka-stt-service-account.json | |
| else | |
| echo "::warning::GOOGLE_CLOUD_CREDENTIALS secret is not set. Speech-to-Text transcription tests will fail." | |
| fi | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Download unit test results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: unit-test-results | |
| path: app/build/ | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| path: app/build/ | |
| - name: Download all instrumented test results (merge to same location) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: instrumented-test-results-shard-* | |
| path: app/build/outputs/ | |
| merge-multiple: true | |
| - name: Debug downloaded artifact structure | |
| run: | | |
| echo "=== Debugging Artifact Download ===" | |
| echo "Checking workspace root for any downloaded files:" | |
| find . -name "coverage-shard-*.ec" -o -name "coverage.ec" | head -20 | |
| echo "" | |
| echo "Checking entire build directory:" | |
| find . -name "*.ec" -type f | head -20 | |
| echo "" | |
| echo "Directory structure of workspace:" | |
| ls -R | head -100 | |
| - name: Verify downloaded artifacts | |
| run: | | |
| echo "Checking for downloaded artifacts..." | |
| echo "" | |
| echo "=== Coverage Files ===" | |
| echo "Unit test coverage (.exec):" | |
| find app/build -name "*.exec" -type f || echo "No .exec files found" | |
| echo "" | |
| echo "Instrumented test coverage (.ec):" | |
| find app/build -name "*.ec" -type f || echo "No .ec files found" | |
| echo "" | |
| echo "Total coverage files: $(find app/build -name "*.exec" -o -name "*.ec" | wc -l)" | |
| echo "" | |
| echo "=== Compiled Classes ===" | |
| find app/build/tmp/kotlin-classes/debug -type f | head -5 || echo "No compiled classes found" | |
| echo "" | |
| echo "=== Lint Report ===" | |
| ls -la app/build/reports/lint-results-debug.xml 2>/dev/null || echo "Lint report not found" | |
| - name: Generate Coverage Report | |
| run: | | |
| echo "About to generate Jacoco report..." | |
| echo "Coverage files that Jacoco should find:" | |
| echo "Pattern: app/build/outputs/unit_test_code_coverage/**/*.exec" | |
| find app/build/outputs/unit_test_code_coverage -name "*.exec" -type f 2>/dev/null || echo "No .exec files" | |
| echo "" | |
| echo "Pattern: app/build/outputs/code_coverage/**/*.ec" | |
| find app/build/outputs/code_coverage -name "*.ec" -type f 2>/dev/null || echo "No .ec files" | |
| echo "" | |
| echo "Generating report..." | |
| ./gradlew jacocoTestReport --info 2>&1 | tee jacoco-output.log | |
| echo "" | |
| echo "Jacoco report generation complete" | |
| echo "Checking if report was generated:" | |
| ls -la app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml || echo "Report XML not found" | |
| - name: Upload Coverage Report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: | | |
| app/build/reports/jacoco/jacocoTestReport/** | |
| compression-level: 9 | |
| - name: Upload report to SonarCloud | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| run: ./gradlew sonar --parallel --build-cache |