Skip to content

test: add Android emulator test for dotnet signal handling#1574

Merged
jpnurmi merged 29 commits intojpnurmi/fix/sa-nodefer-chain-at-startfrom
jpnurmi/test/android-dotnet-signals
Mar 13, 2026
Merged

test: add Android emulator test for dotnet signal handling#1574
jpnurmi merged 29 commits intojpnurmi/fix/sa-nodefer-chain-at-startfrom
jpnurmi/test/android-dotnet-signals

Conversation

@jpnurmi
Copy link
Collaborator

@jpnurmi jpnurmi commented Mar 12, 2026

Extends the existing test_dotnet_signals with an Android emulator test for:

Builds as an Android APK, run on an emulator, and verify CHAIN_AT_START signal handling with Mono:

  • Handled managed exception (NRE): Mono catches the SIGSEGV and converts it to a NullReferenceException, no native crash registered
  • Unhandled managed exception: Mono calls exit(1), no native crash registered (sentry-dotnet handles this at the managed layer via UnhandledExceptionRaiser)
  • Native crash (SIGSEGV in libcrash.so): crash envelope produced

Reproduced the following test failure (before the base was changed to #1572):

FAILED tests/test_dotnet_signals.py::test_android_signals_inproc - AssertionError: Crash marker missing

Note

Turns out CHAIN_AT_START requires API 26+ (tombstoned). With pre-API 26, debuggerd kills the process before the signal handler can run.

@jpnurmi jpnurmi force-pushed the jpnurmi/test/android-dotnet-signals branch 2 times, most recently from f683589 to 482b343 Compare March 13, 2026 16:13
@jpnurmi jpnurmi marked this pull request as ready for review March 13, 2026 17:14
@jpnurmi jpnurmi changed the base branch from master to jpnurmi/fix/sa-nodefer-chain-at-start March 13, 2026 17:14
jpnurmi and others added 26 commits March 13, 2026 18:16
Extends the existing dotnet_signal fixture to build as an Android APK
and run on an emulator. The test verifies CHAIN_AT_START signal handling
with Mono:

- Handled managed exception (NRE): no native crash registered
- Unhandled managed exception: Mono aborts, native crash registered
- Native crash (SIGSEGV in libcrash.so): crash envelope produced

The fixture csproj now multi-targets net10.0 and net10.0-android.
Program.cs exposes RunTest() for the Android MainActivity entry point.
Database state is checked directly on-device via adb run-as.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI sets TEST_X86/ANDROID_API/RUN_ANALYZER to empty strings. Python's
`or` chain returns the last falsy value (empty string ""), which pytest
then tries to compile() as a legacy condition expression, causing
SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Desktop CI runners have .NET 10 but no android workload. MSBuild
validates all TFMs even when building for a specific one, causing
NETSDK1147. Conditionally add the android TFM only when ANDROID_HOME
is present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
run-as can't find the `test` binary on older API levels. Use sh -c
with a single quoted command string so shell builtins are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
am start is asynchronous — it returns before the process starts.
The pidof poll could find no process yet and break immediately,
causing assertions to run before sentry_init, and the finally
block's adb uninstall to kill the app mid-startup (deletePackageX).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Microsoft.NET.Sdk (plain) compiles all .cs files regardless of
directory conventions. Exclude Platforms/Android/ when not building
for an Android target framework.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OnCreate fires before the activity reports launch completion, so if the
test crashes the app, am start -W blocks indefinitely on older API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use wait_for() with polling instead of fixed sleeps to handle timing
variations across emulator API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Post ensures OnResume returns before the test runs (so am start -W
completes). Worker thread avoids Android's main thread uncaught
exception handler killing the process with SIGKILL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move test back to OnResume with worker thread (no DecorView.Post).
Silently handle am start -W timeout since older API levels may not
report activity launch completion before the app exits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run the test on the main thread via Handler.Post and catch unhandled
managed exceptions with try-catch + abort(), matching how MAUI handles
them. This ensures sentry-native's signal handler captures the crash
across all Android API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unhandled managed exceptions on Android go through Mono's exit(1), not
a catchable signal. sentry-dotnet handles these at the managed layer
via UnhandledExceptionRaiser, so sentry-native should not register a
crash. Remove the abort() workaround and align expectations with the
desktop test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch emulator target from default to google_apis to fix stuck
emulator boots on API 23-25. Drop API 27 which has no google_apis
x86_64 system image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-tombstoned Android (API < 26) uses debuggerd which kills the
process before sentry-native's signal handler can run when using
CHAIN_AT_START strategy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jpnurmi and others added 2 commits March 13, 2026 18:16
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jpnurmi jpnurmi force-pushed the jpnurmi/test/android-dotnet-signals branch from 9cdb64a to d337b49 Compare March 13, 2026 17:16
ANDROID_ARCH: x86_64
- name: Android (API 31, NDK 27)
os: macos-15-large
os: ubuntu-latest
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left one of the Android builds (API 35) running with macos-15-large and changed the rest to use ubuntu-latest, which is much cheaper, faster, and works fine with KVM. The host doesn't really matter, but it's nice to have both in the CI to ensure the test suite works on either.

The find command uses single-quoted '*.envelope' glob pattern which
broke when wrapped in single-quoted sh -c.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jpnurmi jpnurmi requested a review from supervacuus March 13, 2026 17:45
Copy link
Collaborator

@supervacuus supervacuus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beauty ❤️

@jpnurmi jpnurmi merged commit 9547e67 into jpnurmi/fix/sa-nodefer-chain-at-start Mar 13, 2026
49 checks passed
@jpnurmi jpnurmi deleted the jpnurmi/test/android-dotnet-signals branch March 13, 2026 20:20
jpnurmi added a commit that referenced this pull request Mar 14, 2026
* test: add Android emulator test for dotnet signal handling

Extends the existing dotnet_signal fixture to build as an Android APK
and run on an emulator. The test verifies CHAIN_AT_START signal handling
with Mono:

- Handled managed exception (NRE): no native crash registered
- Unhandled managed exception: Mono aborts, native crash registered
- Native crash (SIGSEGV in libcrash.so): crash envelope produced

The fixture csproj now multi-targets net10.0 and net10.0-android.
Program.cs exposes RunTest() for the Android MainActivity entry point.
Database state is checked directly on-device via adb run-as.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: wrap skipif conditions in bool() to avoid pytest string evaluation

CI sets TEST_X86/ANDROID_API/RUN_ANALYZER to empty strings. Python's
`or` chain returns the last falsy value (empty string ""), which pytest
then tries to compile() as a legacy condition expression, causing
SyntaxError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: install .NET SDK and Android workload for Android CI jobs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: only target net10.0-android when ANDROID_HOME is set

Desktop CI runners have .NET 10 but no android workload. MSBuild
validates all TFMs even when building for a specific one, causing
NETSDK1147. Conditionally add the android TFM only when ANDROID_HOME
is present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use ANDROID_API instead of ANDROID_HOME for android TFM condition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: print logcat output for Android test diagnostics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use sh -c for run-as commands on Android

run-as can't find the `test` binary on older API levels. Use sh -c
with a single quoted command string so shell builtins are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: include stdout/stderr in dotnet run returncode assertions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use am start -W to avoid race in Android test

am start is asynchronous — it returns before the process starts.
The pidof poll could find no process yet and break immediately,
causing assertions to run before sentry_init, and the finally
block's adb uninstall to kill the app mid-startup (deletePackageX).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: exclude Android sources from desktop dotnet build

Microsoft.NET.Sdk (plain) compiles all .cs files regardless of
directory conventions. Exclude Platforms/Android/ when not building
for an Android target framework.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* try arm64-v8a

* try x86_64 on linux

* Test 22-30

* fix: move test logic to OnResume to avoid am start -W hang

OnCreate fires before the activity reports launch completion, so if the
test crashes the app, am start -W blocks indefinitely on older API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: use default emulator target instead of google_apis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace sleeps with retry loops in Android test

Use wait_for() with polling instead of fixed sleeps to handle timing
variations across emulator API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add timeout to am start -W with logcat dump on failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: run test directly on UI thread instead of background thread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use DecorView.Post + worker thread for Android test

Post ensures OnResume returns before the test runs (so am start -W
completes). Worker thread avoids Android's main thread uncaught
exception handler killing the process with SIGKILL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: simplify Android test app and handle am start -W timeout

Move test back to OnResume with worker thread (no DecorView.Post).
Silently handle am start -W timeout since older API levels may not
report activity launch completion before the app exits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: emulate MAUI abort behavior for unhandled exceptions on Android

Run the test on the main thread via Handler.Post and catch unhandled
managed exceptions with try-catch + abort(), matching how MAUI handles
them. This ensures sentry-native's signal handler captures the crash
across all Android API levels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: expect no crash for unhandled managed exceptions on Android

Unhandled managed exceptions on Android go through Mono's exit(1), not
a catchable signal. sentry-dotnet handles these at the managed layer
via UnhandledExceptionRaiser, so sentry-native should not register a
crash. Remove the abort() workaround and align expectations with the
desktop test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: drop broken Android API 22 job

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: switch Android emulator to google_apis, drop API 27

Switch emulator target from default to google_apis to fix stuck
emulator boots on API 23-25. Drop API 27 which has no google_apis
x86_64 system image.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: skip Android dotnet signal test on API < 26

Pre-tombstoned Android (API < 26) uses debuggerd which kills the
process before sentry-native's signal handler can run when using
CHAIN_AT_START strategy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Try macos-15-large again, drop others but 26

* test: clean up dotnet signal test assertions and comments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: switch Android emulator back to default target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use double quotes in run-as shell wrapper to preserve inner quotes

The find command uses single-quoted '*.envelope' glob pattern which
broke when wrapped in single-quoted sh -c.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants