Skip to content

Commit 9547e67

Browse files
jpnurmiclaude
andauthored
test: add Android emulator test for dotnet signal handling (#1574)
* 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>
1 parent 3ad83bb commit 9547e67

File tree

7 files changed

+297
-21
lines changed

7 files changed

+297
-21
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,17 @@ jobs:
191191
MINGW_ASM_MASM_COMPILER: llvm-ml
192192
MINGW_ASM_MASM_FLAGS: -m64
193193
- name: Android (API 21, NDK 23)
194-
os: macos-15-large
194+
os: ubuntu-latest
195195
ANDROID_API: 21
196196
ANDROID_NDK: 23.2.8568313
197197
ANDROID_ARCH: x86_64
198+
- name: Android (API 26, NDK 27)
199+
os: ubuntu-latest
200+
ANDROID_API: 26
201+
ANDROID_NDK: 27.3.13750724
202+
ANDROID_ARCH: x86_64
198203
- name: Android (API 31, NDK 27)
199-
os: macos-15-large
204+
os: ubuntu-latest
200205
ANDROID_API: 31
201206
ANDROID_NDK: 27.3.13750724
202207
ANDROID_ARCH: x86_64
@@ -242,12 +247,12 @@ jobs:
242247
cache: "pip"
243248

244249
- name: Check Linux CC/CXX
245-
if: ${{ runner.os == 'Linux' && !matrix.container }}
250+
if: ${{ runner.os == 'Linux' && !env['ANDROID_API'] &&!matrix.container }}
246251
run: |
247252
[ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; }
248253
249254
- name: Installing Linux Dependencies
250-
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !matrix.container }}
255+
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['ANDROID_API'] && !matrix.container }}
251256
run: |
252257
sudo apt update
253258
# Install common dependencies
@@ -278,7 +283,7 @@ jobs:
278283
sudo make install
279284
280285
- name: Installing Linux 32-bit Dependencies
281-
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !matrix.container }}
286+
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !env['ANDROID_API'] &&!matrix.container }}
282287
run: |
283288
sudo dpkg --add-architecture i386
284289
sudo apt update
@@ -357,6 +362,22 @@ jobs:
357362
with:
358363
gradle-home-cache-cleanup: true
359364

365+
- name: Setup .NET for Android
366+
if: ${{ env['ANDROID_API'] }}
367+
uses: actions/setup-dotnet@v5
368+
with:
369+
dotnet-version: '10.0.x'
370+
371+
- name: Install .NET Android workload
372+
if: ${{ env['ANDROID_API'] }}
373+
run: dotnet workload restore tests/fixtures/dotnet_signal/test_dotnet.csproj
374+
375+
- name: Enable KVM group perms
376+
if: ${{ runner.os == 'Linux' && env['ANDROID_API'] }}
377+
run: |
378+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
379+
sudo udevadm control --reload-rules
380+
sudo udevadm trigger --name-match=kvm
360381
361382
- name: Add sentry.native.test hostname
362383
if: ${{ runner.os == 'Windows' }}
@@ -386,7 +407,7 @@ jobs:
386407
api-level: ${{ env.ANDROID_API }}
387408
ndk: ${{ env.ANDROID_NDK }}
388409
arch: ${{ env.ANDROID_ARCH }}
389-
target: google_apis
410+
target: default
390411
emulator-boot-timeout: 1200
391412
script: |
392413
# Sync emulator clock with host to avoid timestamp assertion failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- Prevent MSBuild from using parent Directory.Build.props -->
2+
<Project />
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Android.App;
2+
using Android.OS;
3+
4+
// Required for "adb shell run-as" to access the app's data directory in Release builds
5+
[assembly: Application(Debuggable = true)]
6+
7+
namespace dotnet_signal;
8+
9+
[Activity(Name = "dotnet_signal.MainActivity", MainLauncher = true)]
10+
public class MainActivity : Activity
11+
{
12+
protected override void OnResume()
13+
{
14+
base.OnResume();
15+
16+
var arg = Intent?.GetStringExtra("arg");
17+
if (!string.IsNullOrEmpty(arg))
18+
{
19+
var databasePath = FilesDir?.AbsolutePath + "/.sentry-native";
20+
21+
// Post to the message queue so the activity finishes starting
22+
// before the crash test runs. Without this, "am start -W" may hang.
23+
new Handler(Looper.MainLooper!).Post(() =>
24+
{
25+
Program.RunTest(new[] { arg }, databasePath);
26+
FinishAndRemoveTask();
27+
Java.Lang.JavaSystem.Exit(0);
28+
});
29+
}
30+
}
31+
}

tests/fixtures/dotnet_signal/Program.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class Program
2020
[DllImport("sentry", EntryPoint = "sentry_options_set_debug")]
2121
static extern IntPtr sentry_options_set_debug(IntPtr options, int debug);
2222

23+
[DllImport("sentry", EntryPoint = "sentry_options_set_database_path")]
24+
static extern void sentry_options_set_database_path(IntPtr options, string path);
25+
2326
[DllImport("sentry", EntryPoint = "sentry_init")]
2427
static extern int sentry_init(IntPtr options);
2528

26-
static void Main(string[] args)
29+
public static void RunTest(string[] args, string? databasePath = null)
2730
{
2831
var githubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") ?? string.Empty;
2932
if (githubActions == "true") {
@@ -38,10 +41,13 @@ static void Main(string[] args)
3841
var options = sentry_options_new();
3942
sentry_options_set_handler_strategy(options, 1);
4043
sentry_options_set_debug(options, 1);
44+
if (databasePath != null)
45+
{
46+
sentry_options_set_database_path(options, databasePath);
47+
}
4148
sentry_init(options);
4249

43-
var doNativeCrash = args is ["native-crash"];
44-
if (doNativeCrash)
50+
if (args.Contains("native-crash"))
4551
{
4652
native_crash();
4753
}
@@ -51,17 +57,24 @@ static void Main(string[] args)
5157
{
5258
Console.WriteLine("dereference a NULL object from managed code");
5359
var s = default(string);
54-
var c = s.Length;
60+
var c = s!.Length;
5561
}
56-
catch (NullReferenceException exception)
62+
catch (NullReferenceException)
5763
{
5864
}
5965
}
6066
else if (args.Contains("unhandled-managed-exception"))
6167
{
6268
Console.WriteLine("dereference a NULL object from managed code (unhandled)");
6369
var s = default(string);
64-
var c = s.Length;
70+
var c = s!.Length;
6571
}
6672
}
73+
74+
#if !ANDROID
75+
static void Main(string[] args)
76+
{
77+
RunTest(args);
78+
}
79+
#endif
6780
}
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>Exe</OutputType>
4-
<TargetFramework>net10.0</TargetFramework>
4+
<TargetFrameworks>net10.0</TargetFrameworks>
5+
<TargetFrameworks Condition="'$(ANDROID_API)' != ''">$(TargetFrameworks);net10.0-android</TargetFrameworks>
56
<ImplicitUsings>enable</ImplicitUsings>
67
<Nullable>enable</Nullable>
78
</PropertyGroup>
9+
10+
<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
11+
<ApplicationId>io.sentry.ndk.dotnet.signal.test</ApplicationId>
12+
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
13+
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
14+
</PropertyGroup>
15+
16+
<ItemGroup Condition="!$(TargetFramework.Contains('-android'))">
17+
<Compile Remove="Platforms\Android\**" />
18+
</ItemGroup>
19+
20+
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
21+
<AndroidNativeLibrary Include="native\**\*.so" />
22+
</ItemGroup>
823
</Project>

tests/test_build_static.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
import os
44
import pytest
5-
from .conditions import has_breakpad, has_crashpad, has_native
5+
from .conditions import has_breakpad, has_crashpad, has_native, is_android
66

77

88
def test_static_lib(cmake):
@@ -16,7 +16,7 @@ def test_static_lib(cmake):
1616
)
1717

1818
# on linux we can use `ldd` to check that we don’t link to `libsentry.so`
19-
if sys.platform == "linux":
19+
if sys.platform == "linux" and not is_android:
2020
output = subprocess.check_output("ldd sentry_example", cwd=tmp_path, shell=True)
2121
assert b"libsentry.so" not in output
2222

0 commit comments

Comments
 (0)