also looking at this
test(mobile): run the generated apps on a simulator and an emulator
#199
26 files
+3009
-293
| @@ -1,156 +1,208 @@ | ||
| 1 | 1 | name: Mobile E2E Testing |
| 2 | 2 | |
| 3 | # Manual only. This workflow has been red since May and cannot go green from | |
| 4 | # a workflow edit: the iOS Zig sources stopped compiling in the 0.17 migration | |
| 5 | # (1b6f285) and need real source work — see the notes in the PR that made this | |
| 6 | # manual. It also runs no end-to-end tests today; every step that would has an | |
| 7 | # `if: hashFiles(...)` guard pointing at a TestApp/project that is not in the | |
| 8 | # tree, so they all skip. Firing it on push meant a red X on unrelated commits | |
| 9 | # for a signal nobody could act on, and two of its path filters | |
| 10 | # (packages/zig/src/ios/**, packages/zig/src/android/**) match directories that | |
| 11 | # do not exist at all. | |
| 3 | # Runs a generated Craft app on a real iOS simulator and a real Android | |
| 4 | # emulator and asserts that a call made from JavaScript reached native code and | |
| 5 | # came back. | |
| 6 | # | |
| 7 | # The version this replaces asserted nothing. Every iOS step that would have | |
| 8 | # tested something was guarded by | |
| 9 | # `if: hashFiles('packages/ios/TestApp/TestApp.xcodeproj') != ''` against a | |
| 10 | # project that has never existed in the tree, and the Android job's only test | |
| 11 | # invocation was gated on `packages/android/gradlew`, which the generator does | |
| 12 | # not write. Both skipped on every run, and the job reported success. Its | |
| 13 | # header also blamed the iOS Zig sources for not compiling since the 0.17 | |
| 14 | # migration (1b6f285); that is no longer true — | |
| 15 | # `zig build build-ios-simulator` succeeds at this commit, and the iOS job | |
| 16 | # below depends on it. | |
| 17 | # | |
| 18 | # What the harness asserts, per platform, is in scripts/mobile-e2e/protocol.ts: | |
| 19 | # a required list of cases that must all run and all pass, covering at least | |
| 20 | # one success path and one rejection path. Nothing here skips. A missing | |
| 21 | # toolchain, an absent device, or an app that printed nothing is a failure with | |
| 22 | # a named cause. | |
| 23 | ||
| 24 | # GitHub Actions does not resolve YAML anchors, so the two path filters are | |
| 25 | # written out rather than shared. Keep them identical. | |
| 12 | 26 | on: |
| 27 | push: | |
| 28 | branches: [main] | |
| 29 | paths: | |
| 30 | - packages/ios/** | |
| 31 | - packages/android/** | |
| 32 | # The mobile Zig sources are flat files under src/, not src/ios/ and | |
| 33 | # src/android/ as the previous filters claimed — those two directories | |
| 34 | # have never existed, so the filter matched nothing. | |
| 35 | - packages/zig/src/ios*.zig | |
| 36 | - packages/zig/src/android*.zig | |
| 37 | - packages/zig/src/mobile*.zig | |
| 38 | - packages/zig/src/bridge_mobile_*.zig | |
| 39 | - packages/zig/src/bridge_android_*.zig | |
| 40 | - packages/zig/src/jni*.zig | |
| 41 | - packages/zig/build.zig | |
| 42 | - scripts/mobile-e2e.ts | |
| 43 | - scripts/mobile-e2e/** | |
| 44 | - .github/workflows/mobile-e2e.yml | |
| 45 | pull_request: | |
| 46 | paths: | |
| 47 | - packages/ios/** | |
| 48 | - packages/android/** | |
| 49 | - packages/zig/src/ios*.zig | |
| 50 | - packages/zig/src/android*.zig | |
| 51 | - packages/zig/src/mobile*.zig | |
| 52 | - packages/zig/src/bridge_mobile_*.zig | |
| 53 | - packages/zig/src/bridge_android_*.zig | |
| 54 | - packages/zig/src/jni*.zig | |
| 55 | - packages/zig/build.zig | |
| 56 | - scripts/mobile-e2e.ts | |
| 57 | - scripts/mobile-e2e/** | |
| 58 | - .github/workflows/mobile-e2e.yml | |
| 13 | 59 | workflow_dispatch: |
| 14 | 60 | |
| 15 | 61 | concurrency: |
| 16 | 62 | group: mobile-e2e-${{ github.head_ref || github.ref }} |
| 17 | 63 | cancel-in-progress: true |
| 18 | 64 | |
| 65 | permissions: | |
| 66 | contents: read | |
| 67 | ||
| 19 | 68 | defaults: |
| 20 | 69 | run: |
| 21 | 70 | shell: bash |
| 22 | 71 | |
| 23 | 72 | jobs: |
| 24 | ios-simulator: | |
| 25 | # name: iOS Simulator Tests | |
| 26 | runs-on: macos-14 # M1 runner for better iOS simulator performance | |
| 73 | # The transcript reader and the required-case list are the part of this | |
| 74 | # harness that can be tested without a device, and nothing ran those tests: | |
| 75 | # every `bun test` in this repository is scoped to a package directory or to | |
| 76 | # one explicit file, and the root `test` script is the Zig suite. So the | |
| 77 | # ratchet that stops the suite being hollowed out was itself unenforced. | |
| 78 | harness: | |
| 79 | name: Harness unit tests | |
| 80 | runs-on: ubuntu-latest | |
| 81 | timeout-minutes: 10 | |
| 27 | 82 | |
| 28 | 83 | steps: |
| 29 | 84 | - uses: actions/checkout@v6 |
| 30 | 85 | |
| 31 | - name: Setup Zig | |
| 86 | - name: Setup Pantry (provides Bun, Zig, etc.) | |
| 32 | 87 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 |
| 33 | - name: Setup Xcode | |
| 34 | uses: maxim-lobanov/setup-xcode@v1 | |
| 35 | with: | |
| 36 | xcode-version: latest-stable | |
| 37 | ||
| 38 | - name: Cache Zig artifacts | |
| 39 | uses: actions/cache@v5 | |
| 40 | with: | |
| 41 | path: | | |
| 42 | ~/.cache/zig | |
| 43 | packages/zig/.zig-cache | |
| 44 | key: ios-${{ runner.os }}-zig-master-${{ hashFiles('packages/zig/build.zig') }} | |
| 45 | 88 | |
| 46 | - name: List available simulators | |
| 47 | run: | | |
| 48 | xcrun simctl list devices available | |
| 89 | - name: Install dependencies | |
| 90 | run: bun install --frozen-lockfile | |
| 49 | 91 | |
| 50 | - name: Boot iOS Simulator | |
| 51 | run: | | |
| 52 | # Find an iPhone 15 simulator | |
| 53 | DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 15" | head -1 | grep -oE '[0-9A-F-]{36}') | |
| 92 | # The explicit path is load-bearing: a bare filter makes bun walk the | |
| 93 | # repository and run out of file descriptors under the hoisted linker. | |
| 94 | # See the note in native-lifecycle.yml. | |
| 95 | - name: Test the report protocol | |
| 96 | run: bun test ./scripts/mobile-e2e/protocol.test.ts | |
| 54 | 97 | |
| 55 | if [ -z "$DEVICE_ID" ]; then | |
| 56 | # Create one if not found | |
| 57 | DEVICE_ID=$(xcrun simctl create "iPhone 15 Test" "iPhone 15" "iOS17.0") | |
| 58 | fi | |
| 98 | ios-simulator: | |
| 99 | name: iOS simulator | |
| 100 | runs-on: macos-15 | |
| 101 | # A hung simulator launch would otherwise burn the six-hour default. The | |
| 102 | # harness has its own per-app timeout; this is the backstop for everything | |
| 103 | # around it. | |
| 104 | timeout-minutes: 45 | |
| 105 | env: | |
| 106 | CRAFT_EVIDENCE_LABEL: ios-simulator | |
| 59 | 107 | |
| 60 | echo "DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV | |
| 108 | steps: | |
| 109 | - uses: actions/checkout@v6 | |
| 61 | 110 | |
| 62 | # Boot the simulator | |
| 63 | xcrun simctl boot "$DEVICE_ID" || true | |
| 111 | - name: Setup Pantry (provides Bun, Zig, etc.) | |
| 112 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 | |
| 64 | 113 | |
| 65 | # Wait for simulator to finish booting. `bootstatus -b` blocks until | |
| 66 | # the device reaches a Booted state, eliminating the flaky fixed sleep. | |
| 67 | xcrun simctl bootstatus "$DEVICE_ID" -b | |
| 114 | - name: Install XcodeGen | |
| 115 | run: brew install xcodegen | |
| 68 | 116 | |
| 69 | - name: First-party Zig dependencies | |
| 70 | uses: ./.github/actions/first-party-zig-deps | |
| 117 | - name: Install dependencies | |
| 118 | run: bun install --frozen-lockfile | |
| 71 | 119 | |
| 72 | - name: Build iOS library | |
| 73 | working-directory: packages/zig | |
| 74 | run: | | |
| 75 | # Build iOS simulator static libraries using dedicated build step | |
| 76 | # (not the default target which builds macOS executables) | |
| 77 | zig build build-ios-simulator -Doptimize=ReleaseSafe -Dmacos-sdk="$(xcrun --sdk iphonesimulator --show-sdk-path)" | |
| 120 | - name: Cache Zig artifacts | |
| 121 | uses: actions/cache@v5 | |
| 122 | with: | |
| 123 | path: | | |
| 124 | ~/.cache/zig | |
| 125 | packages/zig/.zig-cache | |
| 126 | key: mobile-e2e-ios-${{ runner.os }}-${{ hashFiles('packages/zig/build.zig') }} | |
| 78 | 127 | |
| 79 | 128 | - name: First-party Zig dependencies |
| 80 | 129 | uses: ./.github/actions/first-party-zig-deps |
| 81 | 130 | |
| 82 | - name: Run iOS unit tests | |
| 131 | # build-ios-all, not build-ios-simulator: installRuntime() resolves every | |
| 132 | # SDK in RUNTIME_ARCHIVES before it writes anything and throws when one | |
| 133 | # has no archive at all, so a zig-out holding only the simulator slices | |
| 134 | # fails at project generation. It passes locally only because a device | |
| 135 | # archive is usually already sitting there from an earlier build. | |
| 136 | # | |
| 137 | # No -Dmacos-sdk: the iOS steps resolve their SDK through xcrun at | |
| 138 | # configure time (build.zig's iosSdkPath), and the option is read only by | |
| 139 | # desktop and host-test artifacts, so passing it was a no-op. | |
| 140 | - name: Build the Zig iOS runtime | |
| 83 | 141 | working-directory: packages/zig |
| 84 | run: | | |
| 85 | # `zig build test` reports through its exit code. The old form piped | |
| 86 | # it to a file and grepped for "mobile"/"ios", which fails under | |
| 87 | # `set -e` whenever the run is clean and the file is therefore empty. | |
| 88 | zig build test | |
| 89 | ||
| 90 | - name: Build iOS test app | |
| 91 | if: hashFiles('packages/ios/TestApp/TestApp.xcodeproj') != '' | |
| 92 | run: | | |
| 93 | cd packages/ios/TestApp | |
| 94 | xcodebuild build-for-testing \ | |
| 95 | -scheme TestApp \ | |
| 96 | -destination "platform=iOS Simulator,id=$DEVICE_ID" \ | |
| 97 | -configuration Debug \ | |
| 98 | CODE_SIGN_IDENTITY="" \ | |
| 99 | CODE_SIGNING_REQUIRED=NO | |
| 100 | ||
| 101 | - name: Run iOS E2E tests | |
| 102 | if: hashFiles('packages/ios/TestApp/TestApp.xcodeproj') != '' | |
| 103 | run: | | |
| 104 | cd packages/ios/TestApp | |
| 105 | xcodebuild test-without-building \ | |
| 106 | -scheme TestApp \ | |
| 107 | -destination "platform=iOS Simulator,id=$DEVICE_ID" \ | |
| 108 | -configuration Debug | |
| 109 | ||
| 110 | - name: Capture iOS screenshots | |
| 111 | run: | | |
| 112 | mkdir -p screenshots/ios | |
| 113 | ||
| 114 | # Take a screenshot of the simulator | |
| 115 | xcrun simctl io "$DEVICE_ID" screenshot screenshots/ios/simulator.png || true | |
| 116 | ||
| 117 | - name: Shutdown simulator | |
| 118 | if: always() | |
| 119 | run: | | |
| 120 | xcrun simctl shutdown "$DEVICE_ID" || true | |
| 121 | ||
| 122 | - name: Upload iOS screenshots | |
| 142 | run: zig build build-ios-all -Doptimize=ReleaseSafe | |
| 143 | ||
| 144 | - name: Run mobile E2E on the simulator | |
| 145 | id: harness | |
| 146 | run: bun scripts/mobile-e2e.ts --platform ios --ios-runtime packages/zig/zig-out/lib | |
| 147 | ||
| 148 | - name: Upload iOS evidence | |
| 149 | # Not a bare `always()`: when an earlier step fails the harness never | |
| 150 | # runs, the evidence directory is never created, and `if-no-files-found: | |
| 151 | # error` would then add a "No files were found" error on top of the real | |
| 152 | # one. Skipped-means-skipped; ran-and-produced-nothing still fails. | |
| 153 | if: always() && steps.harness.conclusion != 'skipped' | |
| 123 | 154 | uses: actions/upload-artifact@v4 |
| 124 | 155 | with: |
| 125 | name: ios-screenshots | |
| 126 | path: screenshots/ios/ | |
| 127 | retention-days: 7 | |
| 128 | ||
| 129 | - name: Generate iOS test report | |
| 130 | run: | | |
| 131 | echo "## iOS Simulator Tests" >> $GITHUB_STEP_SUMMARY | |
| 132 | echo "" >> $GITHUB_STEP_SUMMARY | |
| 133 | echo "- Simulator: iPhone 15" >> $GITHUB_STEP_SUMMARY | |
| 134 | echo "- iOS Version: 17.0" >> $GITHUB_STEP_SUMMARY | |
| 135 | echo "- Status: Completed" >> $GITHUB_STEP_SUMMARY | |
| 156 | name: mobile-e2e-ios | |
| 157 | # Console logs, build logs, screenshots, the generated project.yml and | |
| 158 | # the exact page that ran. Excludes work/ — the generated projects and | |
| 159 | # their DerivedData are hundreds of megabytes and the interesting | |
| 160 | # parts are copied out. | |
| 161 | path: | | |
| 162 | artifacts/mobile-e2e/ios-simulator/** | |
| 163 | !artifacts/mobile-e2e/ios-simulator/work/** | |
| 164 | # A run that produced no evidence is a broken harness, not a pass. | |
| 165 | if-no-files-found: error | |
| 166 | retention-days: 30 | |
| 136 | 167 | |
| 137 | 168 | android-emulator: |
| 138 | # name: Android Emulator Tests | |
| 169 | name: Android emulator | |
| 139 | 170 | runs-on: ubuntu-latest |
| 171 | timeout-minutes: 60 | |
| 172 | env: | |
| 173 | CRAFT_EVIDENCE_LABEL: android-emulator | |
| 140 | 174 | |
| 141 | 175 | steps: |
| 142 | 176 | - uses: actions/checkout@v6 |
| 143 | 177 | |
| 144 | - name: Setup Zig | |
| 178 | - name: Setup Pantry (provides Bun, Zig, etc.) | |
| 145 | 179 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 |
| 180 | ||
| 146 | 181 | - name: Setup Java |
| 147 | uses: actions/setup-java@v4 | |
| 182 | uses: actions/setup-java@v6 | |
| 148 | 183 | with: |
| 149 | distribution: 'temurin' | |
| 184 | distribution: temurin | |
| 150 | 185 | java-version: '17' |
| 151 | 186 | |
| 152 | 187 | - name: Setup Android SDK |
| 153 | uses: android-actions/setup-android@v3 | |
| 188 | uses: android-actions/setup-android@v4 | |
| 189 | with: | |
| 190 | # The action defaults to `tools platform-tools`, but the obsolete | |
| 191 | # `tools` package is no longer offered by the Android SDK repository. | |
| 192 | packages: platform-tools | |
| 193 | ||
| 194 | # Same platform and build-tools as ci.yml's android-builder, so the | |
| 195 | # emulator job and the compile gate cannot drift onto different SDKs. | |
| 196 | - name: Install Android platform | |
| 197 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 198 | ||
| 199 | - name: Setup Gradle | |
| 200 | uses: gradle/actions/setup-gradle@v6 | |
| 201 | with: | |
| 202 | gradle-version: '8.11.1' | |
| 203 | ||
| 204 | - name: Install dependencies | |
| 205 | run: bun install --frozen-lockfile | |
| 154 | 206 | |
| 155 | 207 | - name: Cache Zig artifacts |
| 156 | 208 | uses: actions/cache@v5 |
| @@ -158,36 +210,25 @@ jobs: | ||
| 158 | 210 | path: | |
| 159 | 211 | ~/.cache/zig |
| 160 | 212 | packages/zig/.zig-cache |
| 161 | key: android-${{ runner.os }}-zig-master-${{ hashFiles('packages/zig/build.zig') }} | |
| 162 | ||
| 163 | - name: Install Android NDK | |
| 164 | run: | | |
| 165 | sdkmanager --install "ndk;26.1.10909125" | |
| 166 | echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> $GITHUB_ENV | |
| 213 | key: mobile-e2e-android-${{ runner.os }}-${{ hashFiles('packages/zig/build.zig') }} | |
| 167 | 214 | |
| 168 | 215 | - name: First-party Zig dependencies |
| 169 | 216 | uses: ./.github/actions/first-party-zig-deps |
| 170 | 217 | |
| 171 | - name: Build Android library | |
| 172 | working-directory: packages/zig | |
| 218 | # The NDK is what supplies bionic. Without it the JNI library builds and | |
| 219 | # then fails at dlopen with `cannot locate symbol "__tls_get_addr"`, which | |
| 220 | # CraftNative catches and turns into a silent fallback to the Kotlin shim | |
| 221 | # — the failure this whole leg exists to catch. | |
| 222 | - name: Install the Android NDK | |
| 173 | 223 | run: | |
| 174 | # Use the dedicated android step, which resolves its own target. | |
| 175 | # Passing -Dtarget steers the *default* install step instead, which | |
| 176 | # builds the desktop binaries — and since an android triple reports | |
| 177 | # os_tag == .linux, that links GTK3/WebKit2GTK against a sysroot that | |
| 178 | # does not exist for Android. | |
| 179 | zig build build-android -Doptimize=ReleaseSafe | |
| 180 | ||
| 181 | - name: First-party Zig dependencies | |
| 182 | uses: ./.github/actions/first-party-zig-deps | |
| 224 | sdkmanager --install "ndk;26.1.10909125" | |
| 225 | echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> "$GITHUB_ENV" | |
| 183 | 226 | |
| 184 | - name: Run Android unit tests | |
| 227 | # Both ABIs. x86_64 is the one the emulator actually loads; arm64-v8a is | |
| 228 | # built so the runtime leg installs the same APK a phone would get. | |
| 229 | - name: Build the Zig Android runtime | |
| 185 | 230 | working-directory: packages/zig |
| 186 | run: | | |
| 187 | # `zig build test` reports through its exit code. The old form piped | |
| 188 | # it to a file and grepped for "mobile"/"android", which fails under | |
| 189 | # `set -e` whenever the run is clean and the file is therefore empty. | |
| 190 | zig build test | |
| 231 | run: zig build build-android-all -Doptimize=ReleaseSafe -Dandroid-ndk="$ANDROID_NDK_HOME" | |
| 191 | 232 | |
| 192 | 233 | - name: Enable KVM |
| 193 | 234 | run: | |
| @@ -202,9 +243,17 @@ jobs: | ||
| 202 | 243 | path: | |
| 203 | 244 | ~/.android/avd/* |
| 204 | 245 | ~/.android/adb* |
| 205 | key: avd-api-34-${{ runner.os }} | |
| 206 | ||
| 207 | - name: Create AVD and run tests | |
| 246 | # The device, not just the API level. With force-avd-creation off the | |
| 247 | # action reuses whatever the cache restored, so a key that ignores | |
| 248 | # arch and profile would keep testing the old device after someone | |
| 249 | # changed them. | |
| 250 | key: avd-api-34-x86_64-pixel_6-${{ runner.os }} | |
| 251 | ||
| 252 | # `script:` is the only place a command can run with the emulator | |
| 253 | # attached, so the harness is invoked from inside it rather than as its | |
| 254 | # own step. | |
| 255 | - name: Run mobile E2E on the emulator | |
| 256 | id: harness | |
| 208 | 257 | uses: reactivecircus/android-emulator-runner@v2 |
| 209 | 258 | with: |
| 210 | 259 | api-level: 34 |
| @@ -214,56 +263,110 @@ jobs: | ||
| 214 | 263 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim |
| 215 | 264 | disable-animations: true |
| 216 | 265 | script: | |
| 217 | echo "Emulator started" | |
| 218 | 266 | adb wait-for-device |
| 219 | mkdir -p screenshots/android | |
| 220 | adb exec-out screencap -p > screenshots/android/emulator.png || true | |
| 221 | if [ -x "packages/android/gradlew" ]; then | |
| 222 | cd packages/android | |
| 223 | ./gradlew test | |
| 224 | else | |
| 225 | echo "Android Gradle wrapper not present; skipping project-level tests" | |
| 226 | fi | |
| 227 | ||
| 228 | - name: Upload Android screenshots | |
| 267 | bun scripts/mobile-e2e.ts --platform android --android-runtime packages/zig/zig-out/android | |
| 268 | ||
| 269 | - name: Upload Android evidence | |
| 270 | if: always() && steps.harness.conclusion != 'skipped' | |
| 229 | 271 | uses: actions/upload-artifact@v4 |
| 230 | 272 | with: |
| 231 | name: android-screenshots | |
| 232 | path: screenshots/android/ | |
| 233 | retention-days: 7 | |
| 273 | name: mobile-e2e-android | |
| 274 | path: | | |
| 275 | artifacts/mobile-e2e/android-emulator/** | |
| 276 | !artifacts/mobile-e2e/android-emulator/work/** | |
| 277 | if-no-files-found: error | |
| 278 | retention-days: 30 | |
| 279 | ||
| 280 | # The Zig runtime's own simulator fixture: a hand-linked four-file app that | |
| 281 | # drives libcraft-ios-simulator directly, with assertions the generated-app | |
| 282 | # suite cannot make — the host hand-off, the error route's escaping, a | |
| 283 | # Keychain round trip, in-process SQLite, and a sensor the simulator does not | |
| 284 | # have refusing rather than fabricating a stream. It has been in the tree, | |
| 285 | # passing, and wired into no workflow. | |
| 286 | ios-zig-slice: | |
| 287 | name: iOS Zig runtime slice | |
| 288 | runs-on: macos-15 | |
| 289 | timeout-minutes: 45 | |
| 290 | ||
| 291 | steps: | |
| 292 | - uses: actions/checkout@v6 | |
| 293 | ||
| 294 | - name: Setup Pantry (provides Bun, Zig, etc.) | |
| 295 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 | |
| 234 | 296 | |
| 235 | - name: Generate Android test report | |
| 297 | - name: Cache Zig artifacts | |
| 298 | uses: actions/cache@v5 | |
| 299 | with: | |
| 300 | path: | | |
| 301 | ~/.cache/zig | |
| 302 | packages/zig/.zig-cache | |
| 303 | key: mobile-e2e-slice-${{ runner.os }}-${{ hashFiles('packages/zig/build.zig') }} | |
| 304 | ||
| 305 | - name: First-party Zig dependencies | |
| 306 | uses: ./.github/actions/first-party-zig-deps | |
| 307 | ||
| 308 | # The script defaults CRAFT_ZIG to a pinned toolchain under | |
| 309 | # ~/.cache/craft-ci-local, which exists on the author's machine and not on | |
| 310 | # a runner. Point it at the one pantry provides. | |
| 311 | - name: Exercise the Zig runtime on a simulator | |
| 312 | id: harness | |
| 313 | # The fixture polls for its late markers against a wall-clock deadline. | |
| 314 | # A runner is several times slower than a laptop here — the first CI run | |
| 315 | # of this job reached marker 36 of 42 before the old iteration-count | |
| 316 | # budget ran out — so give it room. It exits as soon as the markers | |
| 317 | # arrive; the budget only matters when something is genuinely stuck. | |
| 318 | env: | |
| 319 | CRAFT_SLICE_TIMEOUT: '900' | |
| 236 | 320 | run: | |
| 237 | echo "## Android Emulator Tests" >> $GITHUB_STEP_SUMMARY | |
| 238 | echo "" >> $GITHUB_STEP_SUMMARY | |
| 239 | echo "- Emulator: Pixel 6" >> $GITHUB_STEP_SUMMARY | |
| 240 | echo "- API Level: 34" >> $GITHUB_STEP_SUMMARY | |
| 241 | echo "- Status: Completed" >> $GITHUB_STEP_SUMMARY | |
| 321 | CRAFT_ZIG="$(command -v zig)" \ | |
| 322 | packages/ios/fixtures/zig-slice/build-and-run.sh \ | |
| 323 | "$PWD/artifacts/mobile-e2e/ios-zig-slice" | |
| 324 | ||
| 325 | - name: Upload slice evidence | |
| 326 | if: always() && steps.harness.conclusion != 'skipped' | |
| 327 | uses: actions/upload-artifact@v4 | |
| 328 | with: | |
| 329 | name: mobile-e2e-ios-zig-slice | |
| 330 | path: | | |
| 331 | artifacts/mobile-e2e/ios-zig-slice/console.log | |
| 332 | artifacts/mobile-e2e/ios-zig-slice/console.plain | |
| 333 | if-no-files-found: error | |
| 334 | retention-days: 30 | |
| 242 | 335 | |
| 243 | 336 | mobile-summary: |
| 244 | # name: Mobile Test Summary | |
| 245 | needs: [ios-simulator, android-emulator] | |
| 337 | name: Mobile E2E summary | |
| 338 | needs: [harness, ios-simulator, android-emulator, ios-zig-slice] | |
| 246 | 339 | runs-on: ubuntu-latest |
| 247 | 340 | if: always() |
| 248 | 341 | |
| 249 | 342 | steps: |
| 250 | - name: Check test results | |
| 343 | - name: Report | |
| 251 | 344 | run: | |
| 252 | echo "## Mobile E2E Test Summary" >> $GITHUB_STEP_SUMMARY | |
| 253 | echo "" >> $GITHUB_STEP_SUMMARY | |
| 254 | echo "| Platform | Status |" >> $GITHUB_STEP_SUMMARY | |
| 255 | echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY | |
| 256 | echo "| iOS Simulator | $ |" >> $GITHUB_STEP_SUMMARY | |
| 257 | echo "| Android Emulator | $ |" >> $GITHUB_STEP_SUMMARY | |
| 258 | ||
| 259 | - name: Download all screenshots | |
| 260 | uses: actions/download-artifact@v4 | |
| 261 | with: | |
| 262 | path: all-screenshots | |
| 263 | ||
| 264 | - name: Upload combined screenshots | |
| 265 | uses: actions/upload-artifact@v4 | |
| 266 | with: | |
| 267 | name: mobile-screenshots | |
| 268 | path: all-screenshots/ | |
| 269 | retention-days: 30 | |
| 345 | { | |
| 346 | echo "## Mobile E2E" | |
| 347 | echo | |
| 348 | echo "| Leg | Result |" | |
| 349 | echo "|-----|--------|" | |
| 350 | echo "| Harness unit tests | $ |" | |
| 351 | echo "| iOS simulator (generated app) | $ |" | |
| 352 | echo "| Android emulator (generated app) | $ |" | |
| 353 | echo "| iOS Zig runtime slice | $ |" | |
| 354 | } >> "$GITHUB_STEP_SUMMARY" | |
| 355 | ||
| 356 | # The summary job used to be the whole story, and it reported whatever it | |
| 357 | # was given without ever failing. `needs` does not fail a job on a failed | |
| 358 | # dependency when `if: always()` is set, so say it explicitly. | |
| 359 | # | |
| 360 | # `failure` and `skipped`, not `!= success`: with cancel-in-progress on, | |
| 361 | # pushing twice in quick succession cancels the first run, and treating | |
| 362 | # `cancelled` as a failure would put a red X on it — a signal about | |
| 363 | # nothing, which is how the workflow this replaces earned its reputation. | |
| 364 | - name: Fail if any leg failed | |
| 365 | if: >- | |
| 366 | contains(fromJSON('["failure", "skipped"]'), needs.harness.result) | |
| 367 | || contains(fromJSON('["failure", "skipped"]'), needs.ios-simulator.result) | |
| 368 | || contains(fromJSON('["failure", "skipped"]'), needs.android-emulator.result) | |
| 369 | || contains(fromJSON('["failure", "skipped"]'), needs.ios-zig-slice.result) | |
| 370 | run: | | |
| 371 | echo "::error::at least one mobile E2E leg did not succeed" | |
| 372 | exit 1 | |