diff --git a/.github/actions/folio-app/action.yml b/.github/actions/folio-app/action.yml new file mode 100644 index 0000000..f036868 --- /dev/null +++ b/.github/actions/folio-app/action.yml @@ -0,0 +1,81 @@ +name: folio app +description: Install the toolchain folio needs on one platform, and build the app there. + +inputs: + platform: + description: android, ios or web + required: true + +runs: + using: composite + steps: + - name: Set up the JDKs + uses: actions/setup-java@v5 + with: + distribution: temurin + # The metro gradle plugin folio builds with needs a 21 runtime; the + # sidecar toolchain pins 17. Both are installed so gradle can pick. + java-version: | + 17 + 21 + + # The iOS app builds its Kotlin framework through the folio gradle project, + # which configures :app:androidApp, so this is needed off Android too. + # `make sanderling-android` wants it as well, for the sidecar JAR. + - name: Set up Android SDK + if: inputs.platform != 'web' + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 + + - name: Cache Gradle + if: inputs.platform != 'ios' + uses: actions/cache@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: folio-gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + folio-gradle-${{ runner.os }}- + + # idb-companion is not in homebrew-core, only in facebook/homebrew-fb, so + # it has to be named by its full tap path. xcodegen and just are core. + - name: Install idb-companion, xcodegen and just + if: inputs.platform == 'ios' + shell: bash + run: brew install facebook/fb/idb-companion xcodegen just + + # Both asset tarballs are built by the prepare scripts, and the runner + # bundle is an xcodebuild of companion/Sources. Keyed on the scripts and + # the versions the Makefile embeds, so a later run reuses them. This has to + # land before `make sanderling-ios`, which is what consumes them. + - name: Cache the companion and runner bundles + if: inputs.platform == 'ios' + uses: actions/cache@v6 + with: + path: | + internal/driver/ioscompanion/companionassets/assets + internal/driver/ioscompanion/runnerassets/assets + key: ios-assets-${{ runner.os }}-${{ hashFiles('internal/driver/ioscompanion/companionassets/prepare.sh', 'companion/prepare.sh', 'companion/project.yml', 'companion/Sources/**') }} + + # Without this the emulator falls back to software rendering and every + # step costs several seconds. + - name: Enable KVM + if: inputs.platform == 'android' + shell: bash + 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: Build the folio APK + if: inputs.platform == 'android' + shell: bash + working-directory: examples/folio + run: ./gradlew :app:androidApp:assembleDebug + + - name: Build the folio wasmJs app + if: inputs.platform == 'web' + shell: bash + working-directory: examples/folio + run: ./gradlew :app:webApp:wasmJsBrowserDevelopmentExecutableDistribution diff --git a/.github/actions/folio-simulator/action.yml b/.github/actions/folio-simulator/action.yml new file mode 100644 index 0000000..36dcd1f --- /dev/null +++ b/.github/actions/folio-simulator/action.yml @@ -0,0 +1,31 @@ +name: folio on a simulator +description: Boot an iOS simulator, install folio on it, and leave the app stopped. + +inputs: + device: + description: simulator device name + default: iPhone 16 Pro + +runs: + using: composite + steps: + - name: Boot a simulator + shell: bash + run: | + xcrun simctl boot "$IOS_DEVICE" || true + xcrun simctl bootstatus "$IOS_DEVICE" -b + env: + IOS_DEVICE: ${{ inputs.device }} + + - name: Build and install folio + shell: bash + working-directory: examples/folio + run: just ios + env: + IOS_DEVICE: ${{ inputs.device }} + + # `just ios` leaves the app running, and the run's first act is to clear + # its state. Stopping it here means the run always opens the same way. + - name: Stop the app before the run + shell: bash + run: xcrun simctl terminate booted app.folio || true diff --git a/.github/actions/headless-chrome/action.yml b/.github/actions/headless-chrome/action.yml new file mode 100644 index 0000000..223df70 --- /dev/null +++ b/.github/actions/headless-chrome/action.yml @@ -0,0 +1,30 @@ +name: headless chrome +description: Install Chrome and prove it starts headless before a driver depends on it. + +runs: + using: composite + steps: + # stable is setup-chrome v2's own default, spelled out so a new release of + # the action cannot move the browser these jobs drive. The alternative it + # offers is Chrome for Testing latest, which tracks ahead of the channel + # users run. + - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 + with: + chrome-version: stable + + # Ubuntu 24.04 (current ubuntu-latest) restricts unprivileged user + # namespaces via AppArmor, which stops headless Chrome from starting even + # with --no-sandbox: the process launches but never opens its DevTools + # socket. Re-enable them so the driver's Chrome can come up. + - name: Allow Chrome under unprivileged user namespaces + shell: bash + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + + # Fail here with Chrome's own stderr if the browser can't launch, instead + # of letting the driver report an opaque DevTools timeout downstream. + - name: Verify headless Chrome starts + shell: bash + run: | + chrome --version + chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage \ + --dump-dom 'data:text/html,ok' diff --git a/.github/actions/replay-ui-fixture/action.yml b/.github/actions/replay-ui-fixture/action.yml new file mode 100644 index 0000000..11d5be3 --- /dev/null +++ b/.github/actions/replay-ui-fixture/action.yml @@ -0,0 +1,55 @@ +name: replay ui fixture +description: Record a trace with sanderling, then serve it with sanderling replay. + +outputs: + url: + description: the step page of the served run, for a spec to drive + value: ${{ steps.serve.outputs.url }} + +runs: + using: composite + steps: + # A trace with a violation and uncaught exceptions in it, so the UI has + # something to render in every panel the spec looks at. No + # --exit-on-violation here: the run is the fixture, and stopping it at the + # first violation would leave a four-step trace to fuzz. + - name: Record a fixture trace + shell: bash + run: | + python3 -m http.server 8792 --bind 127.0.0.1 \ + --directory test/browser/testdata/throwing & + ready="" + for _ in $(seq 1 30); do + curl -sf http://127.0.0.1:8792/ >/dev/null && { ready=1; break; } + sleep 1 + done + if [ -z "$ready" ]; then + echo "the fixture http server never answered on 127.0.0.1:8792" >&2 + exit 1 + fi + ./bin/sanderling test \ + --platform web \ + --spec test/browser/testdata/throwing/spec.ts \ + --bundle-id http://127.0.0.1:8792/ \ + --duration 5m --max-steps 25 --seed 7 \ + --output runs/fixture + + - name: Serve the trace with sanderling replay + id: serve + shell: bash + run: | + # Flags before the positional argument: Go's flag package stops + # parsing at the first non-flag word. + ./bin/sanderling replay --port 8793 --no-open runs/fixture & + ready="" + for _ in $(seq 1 30); do + curl -sf http://127.0.0.1:8793/api/runs >/dev/null && { ready=1; break; } + sleep 1 + done + if [ -z "$ready" ]; then + echo "sanderling replay never served /api/runs on 127.0.0.1:8793" >&2 + exit 1 + fi + run_id="$(ls runs/fixture | head -1)" + echo "url=http://127.0.0.1:8793/runs/$run_id/steps/1" >> "$GITHUB_OUTPUT" + curl -sf "http://127.0.0.1:8793/runs/$run_id/steps/1" >/dev/null diff --git a/.github/scripts/folio-run-test.sh b/.github/scripts/folio-run-test.sh new file mode 100755 index 0000000..dc9757a --- /dev/null +++ b/.github/scripts/folio-run-test.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# Drives folio-run.sh through a stubbed `sanderling` binary and checks the +# verdict it reaches from each shape of trace. What is under test is the +# classification, not the fuzzer: the stub writes the trace the run would have +# written and exits the code the run would have exited. +# +# folio-run.sh is invoked as `bash -eo pipefail -c