Skip to content

Merge pull request #515 from Eureka-ch/chore/unit-test-conventions #6

Merge pull request #515 from Eureka-ch/chore/unit-test-conventions

Merge pull request #515 from Eureka-ch/chore/unit-test-conventions #6

Workflow file for this run

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