diff --git a/.github/workflows/device-tests.yml b/.github/workflows/device-tests.yml index 5b23f2e..1a617bc 100644 --- a/.github/workflows/device-tests.yml +++ b/.github/workflows/device-tests.yml @@ -5,10 +5,10 @@ name: Device tests # assert OnAdLoaded fires. Reusable (workflow_call) so both the nightly job (published package) # and the MAUI auto-bump gate (source) can invoke it. # -# GATING MODEL — banner is the hard gate. On a headless, software-GPU emulator (all GitHub-hosted -# runners) only the banner format reliably fills; full-screen / native creatives need a GPU-backed -# emulator (-gpu host) to pre-render, so they are advisory here. Set require_all_formats: true only -# when pointing this at a GPU-capable / self-hosted runner. +# GATING MODEL — banner is the hard gate. Measured on the hosted runner (headless, software GPU, +# google_apis): banner and all four full-screen formats load; native/native-video do not, because +# native demo creatives need market:// click resolution and therefore a Play Store system image. +# Set require_all_formats: true only against a google_apis_playstore AVD on a self-hosted runner. on: workflow_call: @@ -87,7 +87,13 @@ jobs: echo "Build props: $props" # -p:TargetFrameworks (not -f) so restore is single-TFM: `-f` leaves restore # cross-targeting, which would fail with NETSDK1147 for the iOS TFM on this runner. - dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -p:TargetFrameworks=net10.0-android $props + # + # EmbedAssembliesIntoApk=true is REQUIRED here. A Debug Android build otherwise uses + # Fast Deployment, which keeps the managed assemblies OUT of the APK and expects the + # IDE / `dotnet build -t:Run` to push them separately. Installing such an APK with + # plain `adb install` yields an app that aborts at startup with + # "No assemblies found in .../.__override__/x86_64" before a single line is logged. + dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -p:TargetFrameworks=net10.0-android -p:EmbedAssembliesIntoApk=true $props apk=$(find "$PWD/tests/Plugin.AdMob.DeviceTests/bin" -name '*-Signed.apk' | head -1) if [ -z "$apk" ]; then apk=$(find "$PWD/tests/Plugin.AdMob.DeviceTests/bin" -name '*.apk' | head -1); fi if [ -z "$apk" ]; then echo "::error::No APK produced"; exit 1; fi @@ -101,6 +107,9 @@ jobs: api-level: 34 arch: x86_64 target: google_apis + # Embedding the assemblies takes the APK from ~13 MB to ~90 MB; give the AVD headroom + # so the install can't fail with INSTALL_FAILED_INSUFFICIENT_STORAGE. + disk-size: 8G force-avd-creation: false disable-animations: true emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot @@ -115,6 +124,16 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} + # .NET for iOS hard-pins a required Xcode and the image's *selected* Xcode can lag it — + # the 2026-07-25 nightly failed with "This version of .NET for iOS (26.5.10301) requires + # Xcode 26.6. The current version of Xcode is 26.5." Images usually carry several Xcodes, + # so select the newest; the listing makes the mismatch obvious if one is still missing. + - name: Select the newest available Xcode + run: | + ls -d /Applications/Xcode*.app || true + sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)" + xcodebuild -version + - uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x diff --git a/tests/Plugin.AdMob.DeviceTests/AdLoadHarness.cs b/tests/Plugin.AdMob.DeviceTests/AdLoadHarness.cs index 7cd668f..1655f4d 100644 --- a/tests/Plugin.AdMob.DeviceTests/AdLoadHarness.cs +++ b/tests/Plugin.AdMob.DeviceTests/AdLoadHarness.cs @@ -11,12 +11,14 @@ namespace Plugin.AdMob.DeviceTests; /// on-screen rendering is what makes this a stable signal for catching upstream binding / /// MAUI drift in the published package. /// -/// Environment note baked in from the maintainer's own testing: on a headless, software-GPU -/// emulator (GitHub-hosted runners have no GPU) only the BANNER format reliably fills. The -/// full-screen and native formats need a GPU-backed emulator (-gpu host) to pre-render their -/// creative and otherwise come back as no-fill. Hence two summary lines are emitted: +/// Environment note, measured on the GitHub-hosted runner (headless, software GPU, google_apis +/// image): banner AND the full-screen formats (interstitial, rewarded, rewarded-interstitial, +/// app-open) all load fine. Only NATIVE and NATIVE-VIDEO fail there with "Internal error" — +/// native demo ads are app-install creatives whose click actions need market:// resolution, so +/// they want a Play Store system image (google_apis_playstore), not merely a GPU. Hence two +/// summary lines are emitted: /// SUMMARY_BANNER — the format CI can hard-gate on any runner. -/// SUMMARY_ALL — every format; only meaningful (hard-gate-able) on a -gpu host runner. +/// SUMMARY_ALL — every format; only hard-gate-able on a Play Store image. /// internal static class AdLoadHarness { diff --git a/tests/README.md b/tests/README.md index e3818b7..0710acf 100644 --- a/tests/README.md +++ b/tests/README.md @@ -44,11 +44,19 @@ dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -f n ## The banner-vs-full-screen gating model (important) -On a **headless, software-GPU emulator** — which is every GitHub-hosted runner, since they have -no GPU — only the **banner** format reliably fills. Full-screen (interstitial / rewarded / -rewarded-interstitial / app-open) and native creatives need a **GPU-backed emulator (`-gpu host`)** -to pre-render, and otherwise come back as no-fill. This is a documented, hardware-observed quirk, -not a plugin bug. +Not every format fills in every environment, and that is an environment property rather than a +plugin bug. Measured on the GitHub-hosted runner (headless, software GPU, `google_apis` image): + +| Format | Headless CI (`google_apis`) | Local `-gpu host` + Play Store image | +|---|---|---| +| banner | ✅ | ✅ | +| interstitial / rewarded / rewarded-interstitial / app-open | ✅ | ✅ | +| native, native-video | ❌ `Internal error` | ✅ | + +So the full-screen formats *do* load headless. The one real gap is **native**, which serves +app-install creatives whose click actions need `market://` resolution — that requires a +**Play Store** system image (`google_apis_playstore`), not merely a GPU. Loads are also +occasionally flaky (`Internal error` on a first attempt), which is why each format gets a retry. So the harness emits two summary lines and CI gates accordingly: @@ -56,19 +64,32 @@ So the harness emits two summary lines and CI gates accordingly: - `SUMMARY_ALL` → every format; only hard-gated when `require_all_formats: true`. To get **full-format** coverage, run `device-tests.yml` (or the harness directly) against a -**`-gpu host` emulator on a self-hosted runner** and pass `require_all_formats: true`. The -maintainer's local `plugin_admob_ps` (Play Store) AVD is exactly such an environment. +**Play Store (`google_apis_playstore`) AVD on a self-hosted runner** and pass +`require_all_formats: true`. The maintainer's local `plugin_admob_ps` AVD is exactly that. -Run the harness locally against a booted `-gpu host` emulator: +Run the harness locally against a booted emulator: ```bash -dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -f net10.0-android \ +dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug \ + -p:TargetFrameworks=net10.0-android -p:EmbedAssembliesIntoApk=true \ "-p:AndroidSdkDirectory=%LOCALAPPDATA%\Android\Sdk" "-p:JavaSdkDirectory=%LOCALAPPDATA%\Android\Jdk" adb install -r tests/Plugin.AdMob.DeviceTests/bin/Debug/net10.0-android/com.plugin.admob.devicetests-Signed.apk adb logcat -c && adb shell monkey -p com.plugin.admob.devicetests -c android.intent.category.LAUNCHER 1 adb logcat -s AdMobHarness:I # watch RESULT / SUMMARY_* lines ``` +`EmbedAssembliesIntoApk=true` is **required** whenever you install with `adb install`. Without +it a Debug build uses Fast Deployment, leaves the managed assemblies out of the APK, and the app +aborts at startup with `No assemblies found in .../.__override__/...` before logging anything. + +### Known-broken: the iOS leg + +The iOS simulator job currently cannot build on GitHub's macOS image. .NET for iOS 26.5.10301 +requires Xcode 26.6; the image's *selected* Xcode is 26.5, and while 26.6 is installed it ships +without the macOS platform SDK, so `actool` fails (`SDK "…/MacOSX.sdk" cannot be located`). The +job selects the newest Xcode and stays `continue-on-error`, so it never blocks — the run is still +green. iOS *compilation* is covered by `build.yml`'s `net10.0-ios` leg, which passes. + ## One manual step: make the gates required Workflows run automatically, but marking them **required status checks** is a repository diff --git a/tests/ci/run-android-harness.sh b/tests/ci/run-android-harness.sh index ec94c53..ea9dc5b 100644 --- a/tests/ci/run-android-harness.sh +++ b/tests/ci/run-android-harness.sh @@ -4,16 +4,27 @@ set -euo pipefail # Installs the pre-built device-test APK on the booted emulator, launches it, and scrapes the # harness's result lines from logcat. # -# Banner is the hard gate: it is the one format that reliably fills on a headless, software-GPU -# emulator (GitHub-hosted runners have no GPU). The full set (SUMMARY_ALL) is only enforced when -# REQUIRE_ALL=true — i.e. when running against a GPU-backed / -gpu host emulator (e.g. a -# self-hosted runner) where full-screen and native creatives can actually pre-render and fill. +# Banner is the hard gate. On the hosted runner's google_apis image the full-screen formats load +# too, but native/native-video do not (they need a Play Store image for market:// click +# resolution), so the full set (SUMMARY_ALL) is only enforced when REQUIRE_ALL=true — i.e. against +# a google_apis_playstore AVD on a self-hosted runner. PKG="com.plugin.admob.devicetests" TAG="AdMobHarness" REQUIRE_ALL="${REQUIRE_ALL:-false}" APK="${APK:?APK env var not set}" +# Dumped whenever the harness produces no usable result. Without this a startup crash is +# invisible: the tag-filtered log below is simply empty and says nothing about why. +dump_diagnostics() { + echo "===== app process =====" + adb shell pidof "$PKG" || echo "(app is not running)" + echo "===== fatal / crash =====" + adb logcat -d -s AndroidRuntime:E monodroid:F monodroid-assembly:F DEBUG:F 2>/dev/null | tail -40 || true + echo "===== last 120 log lines (unfiltered) =====" + adb logcat -d 2>/dev/null | tail -120 || true +} + echo "Installing $APK" adb install -r "$APK" @@ -49,11 +60,13 @@ echo "all: ${allline:-}" if [ -z "$banner" ]; then echo "::error::Harness did not report a banner result (app crashed or never finished loading)." + dump_diagnostics exit 1 fi if ! echo "$banner" | grep -q "status=PASS"; then echo "::error::Banner ad failed to load against Google's test ad unit." + dump_diagnostics exit 1 fi