Test iOS Game (Vulkan) #38
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 iOS Game (Vulkan) | |
| run-name: >- | |
| ${{ github.event.inputs.commit-sha != '' && format('Test iOS 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 = Combined .app aggregating all 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 .app (faster build/install/run); false = per-suite .apps' | |
| 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: | |
| # Daily at 02:43 UTC — offset off top-of-hour to avoid GitHub Actions cron load spikes; | |
| # spaced from the other nightlies (03:37–05:17) so they don't compete for the scheduler. | |
| - cron: '43 2 * * *' | |
| concurrency: | |
| group: test-ios-game-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} | |
| cancel-in-progress: true | |
| jobs: | |
| iOS-Tests-Game: | |
| # Scheduled (nightly) runs only fire on repos that opt in via STRIDE_ENABLE_SCHEDULED_CI; | |
| # workflow_call (main.yml path filter / ci-ios label) and dispatch always run. | |
| if: > | |
| github.event_name != 'schedule' | |
| || vars.STRIDE_ENABLE_SCHEDULED_CI == 'true' | |
| name: iOS (Vulkan, ${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}) | |
| # macOS host: iOS Simulator only runs on macOS. macos-26 ships Xcode 26.x (we select 26.5 | |
| # below to match the .NET iOS 26.5 SDK pack's strict version check). macos-15's newest Xcode | |
| # is 26.3, which the 26.5 pack rejects. | |
| runs-on: macos-26 | |
| timeout-minutes: 45 | |
| env: | |
| DOTNET_NUGET_SIGNATURE_VERIFICATION: "false" | |
| # Workaround for paravirt-GPU SimMetalHost crash on macos-26 runners: force MoltenVK to | |
| # never use Metal argument buffers, so the descriptor-set bind path doesn't trigger | |
| # argumentEncoderSetValues on the paravirt driver. SIMCTL_CHILD_ prefix is stripped by | |
| # simctl when launching the sim app. Mode 0 = NEVER (mode 1 still uses them when | |
| # descriptor indexing is required, which didn't avoid the crash). | |
| SIMCTL_CHILD_MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS: "0" | |
| steps: | |
| # 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 iOS + macOS Workloads | |
| # macOS workload is needed because the engine libs cross-target net10.0-macos (for the | |
| # AVFoundation/Metal bring-up work). slnf-driven builds dispatch all TFMs of every project | |
| # they pull in, so the iOS slnf still needs the macOS workload available. | |
| uses: ./.github/actions/stride-workload | |
| with: | |
| workloads: ios macos | |
| - name: Select newest Xcode | |
| # The .NET iOS SDK pack enforces a minimum Xcode version that advances with the workload | |
| # set (the latest set now requires 26.6). Select the newest installed Xcode so the check | |
| # passes, instead of pinning a version the pack later outgrows. | |
| run: | | |
| ls -d /Applications/Xcode_*.app | |
| latest=$(ls -d /Applications/Xcode_*.app | sort -V | tail -1) | |
| echo "Selecting $latest" | |
| sudo xcode-select -s "$latest" | |
| xcodebuild -version | |
| - name: Ensure Xcode toolchain utilities resolve | |
| # Xcode 26.6 on the runner image has a broken `xcodebuild -sdk … -find <tool>`, so the .NET | |
| # macOS/iOS SDK can't locate the utilities it needs to build/link/package the .app | |
| # (install_name_tool, mdimport, actool, …). Symlink each into the active toolchain bin from a | |
| # real copy (Command Line Tools, Xcode's own bin, PATH, or the llvm-named variant); the SDK's | |
| # fallback then finds it there. | |
| run: | | |
| set -x | |
| dev="$(xcode-select -p)" | |
| tcbin="$dev/Toolchains/XcodeDefault.xctoolchain/usr/bin" | |
| for tool in install_name_tool mdimport dsymutil strip lipo bitcode_strip actool ibtool derq; do | |
| xcrun -find "$tool" >/dev/null 2>&1 && continue | |
| for cand in \ | |
| "/Library/Developer/CommandLineTools/usr/bin/$tool" \ | |
| "$dev/usr/bin/$tool" \ | |
| "$(command -v "$tool" 2>/dev/null || true)" \ | |
| "$tcbin/llvm-$tool"; do | |
| [ -x "$cand" ] && { sudo ln -sf "$cand" "$tcbin/$tool"; echo "linked $tool -> $cand"; break; } | |
| done | |
| xcrun -find "$tool" >/dev/null 2>&1 || echo "::warning::could not resolve $tool" | |
| done | |
| - name: Start iOS Simulator (background) | |
| # Kick the simulator boot off early so its ~50s first-boot data migration | |
| # (PreferencesMigrator etc.) happens in parallel with the build. We block on | |
| # readiness in a later step right before tests run. | |
| run: | | |
| set -e | |
| UDID=$(xcrun simctl list devices --json | python3 -c ' | |
| import json, sys | |
| d = json.load(sys.stdin)["devices"] | |
| for runtime in sorted((k for k in d if k.startswith("com.apple.CoreSimulator.SimRuntime.iOS-")), reverse=True): | |
| for x in d[runtime]: | |
| if x.get("isAvailable") and "iPhone" in x["name"]: | |
| print(x["udid"]); sys.exit(0) | |
| sys.exit(1) | |
| ') | |
| if [ -z "$UDID" ]; then | |
| echo "::error::No available iPhone simulator runtime found" | |
| xcrun simctl list devices | |
| exit 1 | |
| fi | |
| echo "Booting $UDID" | |
| xcrun simctl boot $UDID | |
| echo "IOS_SIM_UDID=$UDID" >> $GITHUB_ENV | |
| - name: Resolve build target / suite | |
| run: | | |
| # `project` set (gold-gen: one suite only) → that .csproj's .app (~4 min build). | |
| # Unset → use-combined=true (default): Combined.csproj — all suites in one .app, | |
| # one install, one process run (faster build/testing). | |
| # use-combined=false → slnf build + per-suite .apps + 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 "IOS_BUILD_TARGET=$PROJ" >> "$GITHUB_ENV" | |
| echo "IOS_TEST_SUITES=$(basename "$PROJ" .csproj)" >> "$GITHUB_ENV" | |
| elif [ "$USE_COMBINED" = "true" ]; then | |
| echo "IOS_BUILD_TARGET=sources/tests/Stride.Tests.Combined/Stride.Tests.Combined.csproj" >> "$GITHUB_ENV" | |
| echo "IOS_TEST_SUITES=Stride.Tests.Combined" >> "$GITHUB_ENV" | |
| else | |
| # slnf path: one build evaluates the graph once; per-suite .app bundles land under | |
| # bin/Tests/<Suite>/iOS-Vulkan/<Config>/<Suite>.app. Run step iterates IOS_TEST_SUITES. | |
| echo "IOS_BUILD_TARGET=build/Stride.Tests.Game.iOS.slnf" >> "$GITHUB_ENV" | |
| { | |
| echo "IOS_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: Build iOS test .app bundle | |
| # No explicit RuntimeIdentifier: passing one as a global property leaks into the host | |
| # net10.0 build (used to load assemblies into the AssetCompiler) and crashes its | |
| # ResolveNativeReferences. The iOS SDK infers iossimulator-arm64 on its own for | |
| # net10.0-ios on an arm64 host. | |
| run: | | |
| dotnet build "$IOS_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=iOS | |
| - 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-ios-game | |
| path: build.binlog | |
| if-no-files-found: ignore | |
| - name: Wait for iOS Simulator boot | |
| # Block on the boot we kicked off earlier. If the build took longer than the simulator's | |
| # first-boot migration, this is instant; otherwise we wait the remaining seconds. | |
| run: xcrun simctl bootstatus $IOS_SIM_UDID -b | |
| - name: Run tests on iOS Simulator | |
| shell: pwsh | |
| continue-on-error: ${{ inputs.tolerate-test-failures == true }} | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| New-Item -ItemType Directory -Force -Path TestResults | Out-Null | |
| $config = "${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}" | |
| $filter = "${{ github.event.inputs.test-filter || inputs.test-filter }}" | |
| $repeat = "${{ github.event.inputs.repeat || inputs.repeat || '1' }}" | |
| # IOS_TEST_SUITES is single-line (Combined / project=set) OR multi-line (slnf path). | |
| # Loop over them — one suite means one launch (matches the Combined case identically). | |
| $suites = ($env:IOS_TEST_SUITES -split "`n") | Where-Object { $_ -ne '' } | |
| $worstExit = 0 | |
| foreach ($suite in $suites) { | |
| $appPath = Get-ChildItem -Path "bin/Tests/$suite/iOS-Vulkan/$config" -Recurse -Filter "$suite.app" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if (-not $appPath) { | |
| Write-Host "::warning::$suite.app not built — skipping" | |
| continue | |
| } | |
| # Bundle id lives in the .app's Info.plist. | |
| $plist = Join-Path $appPath.FullName 'Info.plist' | |
| $package = (& /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' $plist).Trim() | |
| Write-Host "::group::Running $suite (bundle=$package)" | |
| $driverArgs = @( | |
| '-Package', $package, | |
| '-Suite', $suite, | |
| '-App', $appPath.FullName, | |
| '-Simulator', $env:IOS_SIM_UDID, | |
| '-ResultsDir', "$PWD/tests/local", | |
| '-TimeoutSeconds', '1800', | |
| '-KeepSimulator', | |
| '-StreamLog' | |
| ) | |
| if ($filter) { $driverArgs += @('-Filter', $filter) } | |
| if ([int]$repeat -gt 1) { $driverArgs += @('-Repeat', $repeat) } | |
| & pwsh tests/ios/run-ios-tests.ps1 @driverArgs | |
| if ($LASTEXITCODE -ne 0 -and $worstExit -eq 0) { $worstExit = $LASTEXITCODE } | |
| Write-Host "::endgroup::" | |
| } | |
| # Flatten per-suite TRX into TestResults/ for the test-reporting action (no recurse). | |
| Get-ChildItem -Path 'tests/local' -Recurse -Filter '*.trx' -ErrorAction SilentlyContinue | | |
| ForEach-Object { Copy-Item $_.FullName 'TestResults/' -Force } | |
| if ($worstExit -ne 0) { | |
| Write-Host "::error::At least one suite exited with non-zero code (worst=$worstExit)" | |
| exit $worstExit | |
| } | |
| - name: Publish Test Report | |
| if: always() | |
| uses: phoenix-actions/test-reporting@v16 | |
| with: | |
| name: 'Test Report: iOS Game (Vulkan)' | |
| path: TestResults/*.trx | |
| reporter: dotnet-trx | |
| output-to: step-summary | |
| list-tests: 'failed' | |
| fail-on-error: ${{ inputs.tolerate-test-failures != true }} | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-results-ios-vulkan | |
| path: TestResults/ | |
| if-no-files-found: ignore | |
| - name: Upload test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-artifacts-ios-vulkan | |
| path: tests/local/ | |
| if-no-files-found: ignore | |
| - name: Collect crash reports | |
| # macOS writes .ips crash reports to ~/Library/Logs/DiagnosticReports for any process | |
| # killed unexpectedly. Captures both the test app's own crash AND SimMetalHost / other | |
| # sim infrastructure crashes that take the test process down by side-effect. | |
| if: always() | |
| run: | | |
| mkdir -p crash-reports | |
| find ~/Library/Logs/DiagnosticReports -type f \( -name '*.ips' -o -name '*.crash' \) -mmin -60 -exec cp {} crash-reports/ \; 2>/dev/null || true | |
| ls -la crash-reports/ | |
| - name: Upload crash reports | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: crash-reports-ios-vulkan | |
| path: crash-reports/ | |
| if-no-files-found: ignore |