Test Android Game (Vulkan) #68
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: Test Android Game (Vulkan) | |
| run-name: >- | |
| ${{ github.event.inputs.commit-sha != '' && format('Test Android Game (Vulkan) @ {0}', github.event.inputs.commit-sha) || '' }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| build-type: | |
| description: Build Configuration | |
| default: Debug | |
| type: choice | |
| options: | |
| - Debug | |
| - Release | |
| test-filter: | |
| description: 'vstest --filter expression forwarded to the on-device runner (blank = all tests)' | |
| type: string | |
| default: '' | |
| project: | |
| description: 'Single test .csproj to build/run (blank = all Android suites)' | |
| type: string | |
| default: '' | |
| commit-sha: | |
| description: Commit SHA to test (default = HEAD of selected branch) | |
| type: string | |
| default: '' | |
| upload-binlog: | |
| description: Capture and upload build binlog (off by default; adds some I/O overhead) | |
| default: false | |
| type: boolean | |
| use-combined: | |
| description: 'Combine all test suites into one APK (faster build/install/run); false = per-suite APKs' | |
| default: true | |
| type: boolean | |
| repeat: | |
| description: 'Rerun the filtered test set up to N times in-process, stop on first failure (flake hunting). 1 = single run (default).' | |
| type: number | |
| default: 1 | |
| workflow_call: | |
| inputs: | |
| build-type: | |
| default: Debug | |
| type: string | |
| test-filter: | |
| default: '' | |
| type: string | |
| project: | |
| default: '' | |
| type: string | |
| commit-sha: | |
| default: '' | |
| type: string | |
| # Internal (callers only, e.g. test-gold-gen): tolerate test failures so generating new gold | |
| # doesn't fail the run. Build failures still fail. Not exposed in the dispatch UI. | |
| tolerate-test-failures: | |
| default: false | |
| type: boolean | |
| upload-binlog: | |
| default: false | |
| type: boolean | |
| use-combined: | |
| default: true | |
| type: boolean | |
| repeat: | |
| type: number | |
| default: 1 | |
| schedule: | |
| - cron: '37 3 * * *' | |
| concurrency: | |
| group: test-android-game-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} | |
| cancel-in-progress: true | |
| jobs: | |
| Android-Tests-Game: | |
| name: Android (Vulkan, ${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}) | |
| # Linux host: KVM-accelerated x86_64 Android emulator is the only path that runs in a | |
| # reasonable amount of time on a hosted runner. macOS-14+ Apple Silicon runners don't | |
| # support nested virt for AOSP emulator; older macOS-13 Intel HAXM is being retired. | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 | |
| env: | |
| STRIDE_GRAPHICS_SOFTWARE_RENDERING: "1" | |
| STRIDE_MAX_PARALLELISM: "8" | |
| DOTNET_NUGET_SIGNATURE_VERIFICATION: "false" | |
| steps: | |
| # Hosted runners vary per VM (free space, CPU model, noisy neighbors); record the | |
| # initial state to diagnose "No space left on device" failures and timing variance. | |
| - name: Report runner resources | |
| run: | | |
| df -h / | |
| nproc | |
| lscpu | grep -E 'Model name|MHz' || true | |
| free -h | |
| # Bootstrap .github so the local composite action below is resolvable pre-checkout. | |
| - uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: .github | |
| - uses: ./.github/actions/stride-checkout | |
| with: | |
| ref: ${{ (github.event.inputs.commit-sha != '' && github.event.inputs.commit-sha) || (inputs.commit-sha != '' && inputs.commit-sha) || github.ref }} | |
| exclude: samples | |
| - name: Install Android Workload | |
| uses: ./.github/actions/stride-workload | |
| with: | |
| workloads: android | |
| - name: Setup Android SDK | |
| # NDK: not installed/pinned — the runner image preinstalls NDKs and each project's | |
| # Android workload targets resolve _AndroidNdkDirectory locally (Stride.Build.Sdk | |
| # strips it from ProjectReference dispatches so the value never fragments build | |
| # cache keys; Stride.Native.targets enforces a minimum version at compile time). | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| cmdline-tools-version: 12266719 | |
| accept-android-sdk-licenses: true | |
| packages: tools platform-tools build-tools;36.0.0 platforms;android-34 system-images;android-34;google_apis;x86_64 | |
| - name: Install Linux host runtime deps (for asset compilation) | |
| run: | | |
| # Asset compilation runs on the Linux host with Vulkan via Lavapipe. Mirrors the | |
| # set used by test-linux-game.yml. | |
| sudo apt-get update | |
| sudo apt-get install -y libvulkan1 libvulkan-dev xvfb lld vulkan-validationlayers libva2 libva-drm2 libva-x11-2 | |
| - name: Start virtual display (asset compiler Vulkan) | |
| run: | | |
| Xvfb :99 -screen 0 1920x1080x24 & | |
| echo "DISPLAY=:99" >> $GITHUB_ENV | |
| - name: Register Lavapipe ICD | |
| run: | | |
| # Use the no-dma_buf Lavapipe variant (runtimes/linux-x64-nodmabuf/native/): gfxstream | |
| # aborts headless on an ICD advertising VK_EXT_external_memory_dma_buf. AssetCompiler | |
| # needs a Vulkan device at build time (Skybox/IBL etc.) and is indifferent to dma_buf, | |
| # so the same ICD serves both; it then drives the emulator's Vulkan path via -gpu host | |
| # -feature ForceGpuHost (gfxstream forwards Vulkan to the host loader, honouring | |
| # VK_DRIVER_FILES). | |
| dotnet restore build/Stride.Tests.Game.Runtime.slnf \ | |
| -p:StridePlatforms=Linux \ | |
| -p:StrideGraphicsApis=Vulkan | |
| LIB=$(find ~/.nuget/packages/stride.dependencies.lavapipe -path "*/runtimes/linux-x64-nodmabuf/native/libvulkan_lvp.so" | head -1) | |
| if [ -z "$LIB" ]; then | |
| echo "::error::Lavapipe libvulkan_lvp.so (no-dma_buf variant) not found in NuGet cache" | |
| exit 1 | |
| fi | |
| LIB_ABS=$(readlink -f "$LIB") | |
| ICD_JSON="$PWD/lvp_icd.json" | |
| echo "{\"file_format_version\":\"1.0.0\",\"ICD\":{\"library_path\":\"$LIB_ABS\",\"api_version\":\"1.3.0\"}}" > "$ICD_JSON" | |
| echo "VK_DRIVER_FILES=$ICD_JSON" >> $GITHUB_ENV | |
| - name: Build Android emulator helper Vulkan layer | |
| # Stamps the host OS into the device name so gold images bucket by emulator host | |
| # (Lavapipe renders slightly differently per host RID, and the guest can't otherwise | |
| # tell which host it's on). | |
| run: pwsh tests/android/stride_android_emu_helper_layer/build-and-install.ps1 | |
| - name: Resolve build target / suite | |
| run: | | |
| # `project` set (gold-gen: one suite only) → that .csproj's APK + run-suites.sh. | |
| # Unset → use-combined=true (default): Combined.csproj — all suites in one APK, | |
| # one install, one process run (faster build/testing). | |
| # use-combined=false → slnf build + per-suite APKs + per-suite install/run. | |
| PROJ="${{ github.event.inputs.project || inputs.project }}" | |
| USE_COMBINED="${{ github.event.inputs.use-combined || inputs.use-combined || 'true' }}" | |
| if [ -n "$PROJ" ]; then | |
| echo "ANDROID_BUILD_TARGET=$PROJ" >> "$GITHUB_ENV" | |
| echo "ANDROID_TEST_SUITES=$(basename "$PROJ" .csproj)" >> "$GITHUB_ENV" | |
| echo "ANDROID_RUN_SCRIPT=tests/android/run-suites.sh" >> "$GITHUB_ENV" | |
| elif [ "$USE_COMBINED" = "true" ]; then | |
| echo "ANDROID_BUILD_TARGET=sources/tests/Stride.Tests.Combined/Stride.Tests.Combined.csproj" >> "$GITHUB_ENV" | |
| echo "ANDROID_RUN_SCRIPT=tests/android/run-combined.sh" >> "$GITHUB_ENV" | |
| else | |
| # slnf path: one build evaluates the graph once; per-suite APKs land under | |
| # bin/Tests/<Suite>/Android-Vulkan/<Config>/. Run step iterates ANDROID_TEST_SUITES. | |
| echo "ANDROID_BUILD_TARGET=build/Stride.Tests.Game.Android.slnf" >> "$GITHUB_ENV" | |
| echo "ANDROID_RUN_SCRIPT=tests/android/run-suites.sh" >> "$GITHUB_ENV" | |
| { | |
| echo "ANDROID_TEST_SUITES<<EOF" | |
| echo "Stride.Engine.NoAssets.Tests" | |
| echo "Stride.Input.Tests" | |
| echo "Stride.Audio.Tests" | |
| echo "Stride.Particles.Tests" | |
| echo "Stride.UI.Tests" | |
| echo "Stride.Navigation.Tests" | |
| echo "Stride.Physics.Tests" | |
| echo "Stride.Engine.Tests" | |
| echo "Stride.Graphics.Tests" | |
| echo "Stride.Graphics.Tests.10_0" | |
| echo "Stride.Graphics.Tests.11_0" | |
| echo "EOF" | |
| } >> "$GITHUB_ENV" | |
| fi | |
| - name: Reclaim disk before build | |
| # The combined APK build embeds compiled assets for all suites and dies on the | |
| # runner's stock ~20 GB free. Drop preinstalled trees this job never touches. | |
| # Keep dotnet (drives the workload), the Android SDK/NDK, and pwsh (helper layer). | |
| # large-packages stays off (slow apt removals); swift/CodeQL have no action toggle. | |
| run: | | |
| df -h / | |
| sudo rm -rf /usr/share/swift /opt/hostedtoolcache/CodeQL | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@v1.3.1 | |
| with: | |
| android: false | |
| dotnet: false | |
| tool-cache: false | |
| large-packages: false | |
| haskell: true | |
| docker-images: true | |
| - name: Build Android test APK(s) | |
| run: | | |
| # Embed assemblies into the APK so the test runner has everything (no Fast Deployment). | |
| dotnet build "$ANDROID_BUILD_TARGET" \ | |
| -nr:false -v:m -p:WarningLevel=0 \ | |
| -p:StrideSkipAutoPack=true \ | |
| ${{ inputs.upload-binlog && '-bl:build.binlog' || '' }} \ | |
| -p:Configuration=${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} \ | |
| -p:StridePlatforms=Android \ | |
| -p:EmbedAssembliesIntoApk=true \ | |
| -p:AndroidUseSharedRuntime=false | |
| - name: Upload build binlog | |
| # failure() so we still get the binlog when Build dies (most useful case) | |
| if: failure() || inputs.upload-binlog | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: build-binlog-android-game | |
| path: build.binlog | |
| if-no-files-found: ignore | |
| - name: Enable KVM | |
| # Hosted ubuntu runners expose /dev/kvm but the device node has restrictive perms | |
| # by default. This udev rule grants 0666 so the emulator can use KVM acceleration. | |
| 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: Run tests on Android emulator | |
| continue-on-error: ${{ inputs.tolerate-test-failures == true }} | |
| # Action handles emulator boot/teardown; we run the per-suite pwsh driver inside. | |
| # API 34 / google_apis / x86_64 — API 36+ (Android 16) crashes Mono JIT on the | |
| # emulator (W^X enforcement), so we pin to 34. | |
| env: | |
| # Empty bypasses gfxstream's built-in ICD overrides (initIcdPaths/setIcdPaths) | |
| # which would clobber VK_DRIVER_FILES with bundled paths. | |
| ANDROID_EMU_VK_ICD: "" | |
| # Activates the host-OS-stamp implicit Vulkan layer installed above. | |
| STRIDE_EMU_LAYER: "1" | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 34 | |
| target: google_apis | |
| arch: x86_64 | |
| force-avd-creation: false | |
| # -no-audio omitted: Stride.Audio.Tests' AudioTrack init segfaults in native code | |
| # if the emulator has no audio backend. Default backend on Linux is none/null | |
| # which is enough to keep AudioTrack from crashing. | |
| # -gpu host + -feature ForceGpuHost route Vulkan through the host loader so it | |
| # honours VK_DRIVER_FILES (set in "Register Lavapipe ICD") and picks our Stride | |
| # Lavapipe. The Linux emulator build has no -use-host-vulkan flag, so this | |
| # combination is its equivalent. | |
| # -partition-size 8192 (MB): default userdata is too small for several ~90 MB test APKs (INSUFFICIENT_STORAGE). | |
| emulator-options: -no-snapshot-load -no-window -no-boot-anim -gpu host -feature ForceGpuHost -partition-size 8192 | |
| disable-animations: true | |
| # android-emulator-runner runs each `script:` line as its own `sh -c`, so multi-line | |
| # shell blocks break with "unexpected end of file". The driver lives in a standalone | |
| # script — Combined or per-suite — chosen by the Resolve step above. | |
| script: bash "$ANDROID_RUN_SCRIPT" "${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}" "${{ github.event.inputs.test-filter || inputs.test-filter }}" "${{ github.event.inputs.repeat || inputs.repeat || '1' }}" | |
| - name: Publish Test Report | |
| if: always() | |
| uses: phoenix-actions/test-reporting@v16 | |
| with: | |
| name: 'Test Report: Android Game (Vulkan)' | |
| path: TestResults/*.trx | |
| reporter: dotnet-trx | |
| output-to: step-summary | |
| list-tests: 'failed' | |
| # Tolerated (gold-gen) test failures shouldn't red the run via the reporter; the Build | |
| # and Test steps still fail normally. Defaults to true for direct / PR-gate runs. | |
| fail-on-error: ${{ inputs.tolerate-test-failures != true }} | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-android-vulkan | |
| path: TestResults/ | |
| if-no-files-found: ignore | |
| - name: Upload test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-artifacts-android-vulkan | |
| # Single path so upload-artifact uses tests/local/ as the common ancestor: per-suite | |
| # outputs land as Stride.<Suite>.Tests/... at the artifact root, matching what | |
| # comparegold and the per-suite (master) flow expect. The driver moves the logcat | |
| # into ResultsDir after the pre-pull wipe is past so it ships with this artifact. | |
| path: tests/local/ | |
| if-no-files-found: ignore |