Skip to content

Commit 64889dc

Browse files
Fix device tests: embed assemblies in the APK so the app can actually start (#118)
* Fix device tests: embed assemblies in the APK so the app can actually start The first nightly run booted the emulator, installed the APK and launched it, then sat for the full 8-minute timeout with an empty harness log. Reproduced locally: the app aborts during startup, before any managed code runs. F monodroid: No assemblies found in '/data/user/0/com.plugin.admob.devicetests/files/.__override__/x86_64'. Assuming this is part of Fast Deployment. Exiting... A Debug Android build uses Fast Deployment, which deliberately leaves the managed assemblies out of the APK for `dotnet build -t:Run` to push separately. Installing that APK with plain `adb install` gives an app with no managed code. Building with -p:EmbedAssembliesIntoApk=true produces a self-contained APK (~13 MB -> ~90 MB), so give the AVD an 8G disk as well — a 91 MB install hit INSTALL_FAILED_INSUFFICIENT_STORAGE on a full local emulator. Verified on a local emulator with the fixed APK: all seven formats load. RESULT format=banner status=PASS RESULT format=app-open status=PASS RESULT format=interstitial status=PASS RESULT format=native status=PASS RESULT format=rewarded status=PASS RESULT format=native-video status=PASS RESULT format=rewarded-interstitial status=PASS SUMMARY_ALL status=PASS passed=7 total=7 Also dump real diagnostics when no result is reported. The gate only ever printed the tag-filtered log, so a startup crash showed up as an empty block that said nothing about the cause; it now prints the process state, fatal/crash lines and unfiltered log tail. And select the newest installed Xcode on the iOS leg. That job 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 with an older one selected. The listing makes it obvious if none is new enough. The job stays advisory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Correct the gating rationale to match what CI actually measured The verification run disproved the documented reason for the banner-only gate. On the hosted runner (headless, software GPU, google_apis image) banner AND all four full-screen formats load: banner PASS interstitial PASS rewarded PASS (2nd attempt) rewarded-interstitial PASS app-open PASS native FAIL "Internal error" native-video FAIL "Internal error" SUMMARY_BANNER status=PASS SUMMARY_ALL status=FAIL passed=5 total=7 So full-screen formats do not need -gpu host to load. The real gap is native, whose demo creatives are app-install ads needing market:// click resolution — that wants a Play Store system image (google_apis_playstore), not a GPU. The gate itself was already right; only the explanation was wrong. Updated in the README, harness docs, gate script and workflow. Also fixed the README's local-repro snippet, which omitted EmbedAssembliesIntoApk and so reproduced the very startup crash this branch fixes, and documented the currently broken iOS leg: Xcode 26.6 is installed on the image but ships without the macOS platform SDK, so actool fails, while .NET for iOS 26.5.10301 refuses to build under 26.5. That job stays advisory — the dispatch run confirmed a failing advisory job still leaves the run green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2f5b3ae commit 64889dc

4 files changed

Lines changed: 78 additions & 23 deletions

File tree

.github/workflows/device-tests.yml

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ name: Device tests
55
# assert OnAdLoaded fires. Reusable (workflow_call) so both the nightly job (published package)
66
# and the MAUI auto-bump gate (source) can invoke it.
77
#
8-
# GATING MODEL — banner is the hard gate. On a headless, software-GPU emulator (all GitHub-hosted
9-
# runners) only the banner format reliably fills; full-screen / native creatives need a GPU-backed
10-
# emulator (-gpu host) to pre-render, so they are advisory here. Set require_all_formats: true only
11-
# when pointing this at a GPU-capable / self-hosted runner.
8+
# GATING MODEL — banner is the hard gate. Measured on the hosted runner (headless, software GPU,
9+
# google_apis): banner and all four full-screen formats load; native/native-video do not, because
10+
# native demo creatives need market:// click resolution and therefore a Play Store system image.
11+
# Set require_all_formats: true only against a google_apis_playstore AVD on a self-hosted runner.
1212

1313
on:
1414
workflow_call:
@@ -87,7 +87,13 @@ jobs:
8787
echo "Build props: $props"
8888
# -p:TargetFrameworks (not -f) so restore is single-TFM: `-f` leaves restore
8989
# cross-targeting, which would fail with NETSDK1147 for the iOS TFM on this runner.
90-
dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -p:TargetFrameworks=net10.0-android $props
90+
#
91+
# EmbedAssembliesIntoApk=true is REQUIRED here. A Debug Android build otherwise uses
92+
# Fast Deployment, which keeps the managed assemblies OUT of the APK and expects the
93+
# IDE / `dotnet build -t:Run` to push them separately. Installing such an APK with
94+
# plain `adb install` yields an app that aborts at startup with
95+
# "No assemblies found in .../.__override__/x86_64" before a single line is logged.
96+
dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -p:TargetFrameworks=net10.0-android -p:EmbedAssembliesIntoApk=true $props
9197
apk=$(find "$PWD/tests/Plugin.AdMob.DeviceTests/bin" -name '*-Signed.apk' | head -1)
9298
if [ -z "$apk" ]; then apk=$(find "$PWD/tests/Plugin.AdMob.DeviceTests/bin" -name '*.apk' | head -1); fi
9399
if [ -z "$apk" ]; then echo "::error::No APK produced"; exit 1; fi
@@ -101,6 +107,9 @@ jobs:
101107
api-level: 34
102108
arch: x86_64
103109
target: google_apis
110+
# Embedding the assemblies takes the APK from ~13 MB to ~90 MB; give the AVD headroom
111+
# so the install can't fail with INSTALL_FAILED_INSUFFICIENT_STORAGE.
112+
disk-size: 8G
104113
force-avd-creation: false
105114
disable-animations: true
106115
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot
@@ -115,6 +124,16 @@ jobs:
115124
with:
116125
ref: ${{ inputs.ref || github.ref }}
117126

127+
# .NET for iOS hard-pins a required Xcode and the image's *selected* Xcode can lag it —
128+
# the 2026-07-25 nightly failed with "This version of .NET for iOS (26.5.10301) requires
129+
# Xcode 26.6. The current version of Xcode is 26.5." Images usually carry several Xcodes,
130+
# so select the newest; the listing makes the mismatch obvious if one is still missing.
131+
- name: Select the newest available Xcode
132+
run: |
133+
ls -d /Applications/Xcode*.app || true
134+
sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)"
135+
xcodebuild -version
136+
118137
- uses: actions/setup-dotnet@v4
119138
with:
120139
dotnet-version: 10.0.x

tests/Plugin.AdMob.DeviceTests/AdLoadHarness.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ namespace Plugin.AdMob.DeviceTests;
1111
/// on-screen rendering is what makes this a stable signal for catching upstream binding /
1212
/// MAUI drift in the published package.
1313
///
14-
/// Environment note baked in from the maintainer's own testing: on a headless, software-GPU
15-
/// emulator (GitHub-hosted runners have no GPU) only the BANNER format reliably fills. The
16-
/// full-screen and native formats need a GPU-backed emulator (-gpu host) to pre-render their
17-
/// creative and otherwise come back as no-fill. Hence two summary lines are emitted:
14+
/// Environment note, measured on the GitHub-hosted runner (headless, software GPU, google_apis
15+
/// image): banner AND the full-screen formats (interstitial, rewarded, rewarded-interstitial,
16+
/// app-open) all load fine. Only NATIVE and NATIVE-VIDEO fail there with "Internal error" —
17+
/// native demo ads are app-install creatives whose click actions need market:// resolution, so
18+
/// they want a Play Store system image (google_apis_playstore), not merely a GPU. Hence two
19+
/// summary lines are emitted:
1820
/// SUMMARY_BANNER — the format CI can hard-gate on any runner.
19-
/// SUMMARY_ALL — every format; only meaningful (hard-gate-able) on a -gpu host runner.
21+
/// SUMMARY_ALL — every format; only hard-gate-able on a Play Store image.
2022
/// </summary>
2123
internal static class AdLoadHarness
2224
{

tests/README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,31 +44,52 @@ dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -f n
4444

4545
## The banner-vs-full-screen gating model (important)
4646

47-
On a **headless, software-GPU emulator** — which is every GitHub-hosted runner, since they have
48-
no GPU — only the **banner** format reliably fills. Full-screen (interstitial / rewarded /
49-
rewarded-interstitial / app-open) and native creatives need a **GPU-backed emulator (`-gpu host`)**
50-
to pre-render, and otherwise come back as no-fill. This is a documented, hardware-observed quirk,
51-
not a plugin bug.
47+
Not every format fills in every environment, and that is an environment property rather than a
48+
plugin bug. Measured on the GitHub-hosted runner (headless, software GPU, `google_apis` image):
49+
50+
| Format | Headless CI (`google_apis`) | Local `-gpu host` + Play Store image |
51+
|---|---|---|
52+
| banner |||
53+
| interstitial / rewarded / rewarded-interstitial / app-open |||
54+
| native, native-video |`Internal error` ||
55+
56+
So the full-screen formats *do* load headless. The one real gap is **native**, which serves
57+
app-install creatives whose click actions need `market://` resolution — that requires a
58+
**Play Store** system image (`google_apis_playstore`), not merely a GPU. Loads are also
59+
occasionally flaky (`Internal error` on a first attempt), which is why each format gets a retry.
5260

5361
So the harness emits two summary lines and CI gates accordingly:
5462

5563
- `SUMMARY_BANNER`**hard gate** on any runner.
5664
- `SUMMARY_ALL` → every format; only hard-gated when `require_all_formats: true`.
5765

5866
To get **full-format** coverage, run `device-tests.yml` (or the harness directly) against a
59-
**`-gpu host` emulator on a self-hosted runner** and pass `require_all_formats: true`. The
60-
maintainer's local `plugin_admob_ps` (Play Store) AVD is exactly such an environment.
67+
**Play Store (`google_apis_playstore`) AVD on a self-hosted runner** and pass
68+
`require_all_formats: true`. The maintainer's local `plugin_admob_ps` AVD is exactly that.
6169

62-
Run the harness locally against a booted `-gpu host` emulator:
70+
Run the harness locally against a booted emulator:
6371

6472
```bash
65-
dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug -f net10.0-android \
73+
dotnet build tests/Plugin.AdMob.DeviceTests/Plugin.AdMob.DeviceTests.csproj -c Debug \
74+
-p:TargetFrameworks=net10.0-android -p:EmbedAssembliesIntoApk=true \
6675
"-p:AndroidSdkDirectory=%LOCALAPPDATA%\Android\Sdk" "-p:JavaSdkDirectory=%LOCALAPPDATA%\Android\Jdk"
6776
adb install -r tests/Plugin.AdMob.DeviceTests/bin/Debug/net10.0-android/com.plugin.admob.devicetests-Signed.apk
6877
adb logcat -c && adb shell monkey -p com.plugin.admob.devicetests -c android.intent.category.LAUNCHER 1
6978
adb logcat -s AdMobHarness:I # watch RESULT / SUMMARY_* lines
7079
```
7180

81+
`EmbedAssembliesIntoApk=true` is **required** whenever you install with `adb install`. Without
82+
it a Debug build uses Fast Deployment, leaves the managed assemblies out of the APK, and the app
83+
aborts at startup with `No assemblies found in .../.__override__/...` before logging anything.
84+
85+
### Known-broken: the iOS leg
86+
87+
The iOS simulator job currently cannot build on GitHub's macOS image. .NET for iOS 26.5.10301
88+
requires Xcode 26.6; the image's *selected* Xcode is 26.5, and while 26.6 is installed it ships
89+
without the macOS platform SDK, so `actool` fails (`SDK "…/MacOSX.sdk" cannot be located`). The
90+
job selects the newest Xcode and stays `continue-on-error`, so it never blocks — the run is still
91+
green. iOS *compilation* is covered by `build.yml`'s `net10.0-ios` leg, which passes.
92+
7293
## One manual step: make the gates required
7394

7495
Workflows run automatically, but marking them **required status checks** is a repository

tests/ci/run-android-harness.sh

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,27 @@ set -euo pipefail
44
# Installs the pre-built device-test APK on the booted emulator, launches it, and scrapes the
55
# harness's result lines from logcat.
66
#
7-
# Banner is the hard gate: it is the one format that reliably fills on a headless, software-GPU
8-
# emulator (GitHub-hosted runners have no GPU). The full set (SUMMARY_ALL) is only enforced when
9-
# REQUIRE_ALL=true — i.e. when running against a GPU-backed / -gpu host emulator (e.g. a
10-
# self-hosted runner) where full-screen and native creatives can actually pre-render and fill.
7+
# Banner is the hard gate. On the hosted runner's google_apis image the full-screen formats load
8+
# too, but native/native-video do not (they need a Play Store image for market:// click
9+
# resolution), so the full set (SUMMARY_ALL) is only enforced when REQUIRE_ALL=true — i.e. against
10+
# a google_apis_playstore AVD on a self-hosted runner.
1111

1212
PKG="com.plugin.admob.devicetests"
1313
TAG="AdMobHarness"
1414
REQUIRE_ALL="${REQUIRE_ALL:-false}"
1515
APK="${APK:?APK env var not set}"
1616

17+
# Dumped whenever the harness produces no usable result. Without this a startup crash is
18+
# invisible: the tag-filtered log below is simply empty and says nothing about why.
19+
dump_diagnostics() {
20+
echo "===== app process ====="
21+
adb shell pidof "$PKG" || echo "(app is not running)"
22+
echo "===== fatal / crash ====="
23+
adb logcat -d -s AndroidRuntime:E monodroid:F monodroid-assembly:F DEBUG:F 2>/dev/null | tail -40 || true
24+
echo "===== last 120 log lines (unfiltered) ====="
25+
adb logcat -d 2>/dev/null | tail -120 || true
26+
}
27+
1728
echo "Installing $APK"
1829
adb install -r "$APK"
1930

@@ -49,11 +60,13 @@ echo "all: ${allline:-<none>}"
4960

5061
if [ -z "$banner" ]; then
5162
echo "::error::Harness did not report a banner result (app crashed or never finished loading)."
63+
dump_diagnostics
5264
exit 1
5365
fi
5466

5567
if ! echo "$banner" | grep -q "status=PASS"; then
5668
echo "::error::Banner ad failed to load against Google's test ad unit."
69+
dump_diagnostics
5770
exit 1
5871
fi
5972

0 commit comments

Comments
 (0)