From 70426744bca84bdb47f17efe29c8334761e56e7c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Nov 2025 16:31:10 +0200 Subject: [PATCH 01/52] Update sample app to receive input args on Android (required for testing) --- sample/Config/DefaultEngine.ini | 2 + .../SentryPlayground.Build.cs | 12 +++- .../SentryPlaygroundGameInstance.cpp | 70 +++++++++++++++++-- .../SentryPlayground_Android_UPL.xml | 49 +++++++++++++ 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml diff --git a/sample/Config/DefaultEngine.ini b/sample/Config/DefaultEngine.ini index bca84ad7e..1e8177a5d 100644 --- a/sample/Config/DefaultEngine.ini +++ b/sample/Config/DefaultEngine.ini @@ -215,6 +215,8 @@ KeyStore=debug.keystore KeyAlias=androiddebugkey KeyStorePassword=android KeyPassword=android +bBuildForArm64=True +bBuildForX8664=True [/Script/IOSRuntimeSettings.IOSRuntimeSettings] BundleIdentifier=io.sentry.unreal.sample diff --git a/sample/Source/SentryPlayground/SentryPlayground.Build.cs b/sample/Source/SentryPlayground/SentryPlayground.Build.cs index f1d2de319..ae2b24458 100644 --- a/sample/Source/SentryPlayground/SentryPlayground.Build.cs +++ b/sample/Source/SentryPlayground/SentryPlayground.Build.cs @@ -1,23 +1,31 @@ // Copyright (c) 2025 Sentry. All Rights Reserved. using UnrealBuildTool; +using System.IO; public class SentryPlayground : ModuleRules { public SentryPlayground(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Sentry" }); PrivateDependencyModuleNames.AddRange(new string[] { }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); - + // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true + + // Register Android UPL for Intent extras handling (integration tests) + if (Target.Platform == UnrealTargetPlatform.Android) + { + string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath); + AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "SentryPlayground_Android_UPL.xml")); + } } } diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index 989b52943..64aa7d9dc 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -14,18 +14,80 @@ #include "Misc/CommandLine.h" #include "Engine/Engine.h" +#if PLATFORM_ANDROID +#include "Android/AndroidJNI.h" +#include "Android/AndroidApplication.h" + +// Helper function to get command-line arguments from Android Intent extras +// Calls the AndroidThunkJava_GetIntentCommandLine() method added via UPL +FString GetIntentCommandLineFromAndroid() +{ + FString Result; + + if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) + { + // Get GameActivity class + jclass GameActivityClass = FAndroidApplication::FindJavaClass("com/epicgames/unreal/GameActivity"); + if (GameActivityClass != nullptr) + { + // Find our custom method: AndroidThunkJava_GetIntentCommandLine() + jmethodID GetIntentCmdLineMethod = Env->GetMethodID( + GameActivityClass, + "AndroidThunkJava_GetIntentCommandLine", + "()Ljava/lang/String;" + ); + + if (GetIntentCmdLineMethod != nullptr) + { + // Get the GameActivity instance + jobject GameActivityObj = FAndroidApplication::GetGameActivityThis(); + if (GameActivityObj != nullptr) + { + // Call the method + jstring JavaResult = (jstring)Env->CallObjectMethod(GameActivityObj, GetIntentCmdLineMethod); + if (JavaResult != nullptr) + { + // Convert Java string to FString + const char* JavaChars = Env->GetStringUTFChars(JavaResult, nullptr); + Result = FString(UTF8_TO_TCHAR(JavaChars)); + Env->ReleaseStringUTFChars(JavaResult, JavaChars); + Env->DeleteLocalRef(JavaResult); + } + } + } + + Env->DeleteLocalRef(GameActivityClass); + } + } + + return Result; +} +#endif + void USentryPlaygroundGameInstance::Init() { Super::Init(); - const TCHAR* CommandLine = FCommandLine::Get(); + FString CommandLine = FCommandLine::Get(); + +#if PLATFORM_ANDROID + // On Android, merge Intent extras into command line + // Intent extras are passed via: adb shell am start -n / -e test crash-capture + FString IntentCommandLine = GetIntentCommandLineFromAndroid(); + if (!IntentCommandLine.IsEmpty()) + { + UE_LOG(LogSentrySample, Display, TEXT("[SentryPlayground] Intent command line: %s"), *IntentCommandLine); + // Prepend Intent args so they take precedence over default command line + CommandLine = IntentCommandLine + TEXT(" ") + CommandLine; + } +#endif // Check for expected test parameters to decide between running integration tests // or launching the sample app with UI for manual testing - if (FParse::Param(FCommandLine::Get(), TEXT("crash-capture")) || - FParse::Param(FCommandLine::Get(), TEXT("message-capture"))) + if (FParse::Param(*CommandLine, TEXT("crash-capture")) || + FParse::Param(*CommandLine, TEXT("message-capture"))) { - RunIntegrationTest(CommandLine); + RunIntegrationTest(*CommandLine); } } diff --git a/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml b/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml new file mode 100644 index 000000000..0e0de826d --- /dev/null +++ b/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml @@ -0,0 +1,49 @@ + + + + + + + + +// Public method to extract Intent extras and return as command-line string +// Called from C++ via JNI in SentryPlaygroundGameInstance +public String AndroidThunkJava_GetIntentCommandLine() { + android.content.Intent intent = getIntent(); + if (intent == null) { + return ""; + } + + StringBuilder cmdLine = new StringBuilder(); + + // Extract test type from Intent extras + // Usage: adb shell am start -n <package>/<activity> -e test crash-capture + String testType = intent.getStringExtra("test"); + if (testType != null) { + if (testType.equals("crash-capture")) { + cmdLine.append("-crash-capture "); + } else if (testType.equals("message-capture")) { + cmdLine.append("-message-capture "); + } + } + + // Extract optional DSN override + // Usage: adb shell am start -n <package>/<activity> -e dsn "https://..." + String dsn = intent.getStringExtra("dsn"); + if (dsn != null) { + cmdLine.append("-dsn=\"").append(dsn).append("\" "); + } + + // Add standard test flags for headless execution + cmdLine.append("-nullrhi -unattended -stdout -nosplash"); + + String result = cmdLine.toString().trim(); + if (!result.isEmpty()) { + android.util.Log.d("SentryPlayground", "Intent command line: " + result); + } + + return result; +} + + + \ No newline at end of file From 6e34b25c3df54f7fa01d66f19f9725d72b38596f Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Nov 2025 16:31:26 +0200 Subject: [PATCH 02/52] Add Android integration tests to CI --- .github/workflows/ci.yml | 13 + .../workflows/integration-test-android.yml | 86 ++++ .../Integration.Tests.Android.ps1 | 366 ++++++++++++++++++ 3 files changed, 465 insertions(+) create mode 100644 .github/workflows/integration-test-android.yml create mode 100644 integration-test/Integration.Tests.Android.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0c0f0aea..bb617ff43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,3 +198,16 @@ jobs: uses: ./.github/workflows/integration-test-windows.yml with: unreal-version: ${{ matrix.unreal }} + + integration-test-android: + needs: [test-android] + name: Android UE ${{ matrix.unreal }} + secrets: inherit + strategy: + fail-fast: false + matrix: + # Starting with UE 5.4-5.6 for faster iteration + unreal: ['5.4', '5.5', '5.6'] + uses: ./.github/workflows/integration-test-android.yml + with: + unreal-version: ${{ matrix.unreal }} diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml new file mode 100644 index 000000000..07ee2ccdf --- /dev/null +++ b/.github/workflows/integration-test-android.yml @@ -0,0 +1,86 @@ +on: + workflow_call: + inputs: + unreal-version: + required: true + type: string + +jobs: + integration-test: + name: Integration Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download sample build + uses: actions/download-artifact@v4 + with: + name: UE ${{ inputs.unreal-version }} sample build (Android) + path: sample-build + + - name: List downloaded files + run: | + echo "Downloaded build contents:" + ls -lah sample-build/ + echo "Using x64 APK for emulator testing" + + - name: Enable KVM group perms + 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: Setup Android directories + run: | + mkdir -p $HOME/.android/avd + touch $HOME/.android/repositories.cfg + + - name: Install Pester + shell: pwsh + run: Install-Module -Name Pester -Force -SkipPublisherCheck + + - name: Setup integration test configuration + shell: pwsh + run: | + cd integration-test + mkdir build + cmake -B build -S . + + - name: Run Android Integration Tests + uses: reactivecircus/android-emulator-runner@v2 + id: integration-test + timeout-minutes: 45 + with: + api-level: 34 + target: 'google_apis' + arch: x86_64 + force-avd-creation: true + disable-animations: true + disable-spellchecker: true + emulator-options: > + -no-window + -no-snapshot-save + -gpu swiftshader_indirect + -noaudio + -no-boot-anim + -camera-back none + -camera-front none + script: | + adb wait-for-device + cd integration-test + pwsh -Command "Invoke-Pester Integration.Tests.Android.ps1 -CI" + env: + SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} + SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build + + - name: Upload integration test output + if: ${{ always() && steps.integration-test.outcome == 'failure' }} + uses: actions/upload-artifact@v4 + with: + name: UE ${{ inputs.unreal-version }} integration test output (Android) + path: integration-test/output/ + retention-days: 14 \ No newline at end of file diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 new file mode 100644 index 000000000..e794005f5 --- /dev/null +++ b/integration-test/Integration.Tests.Android.ps1 @@ -0,0 +1,366 @@ +# Integration tests for Sentry Unreal SDK on Android +# Requires: +# - Pre-built APK (x64 for emulator) +# - Android emulator or device connected +# - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Get the x64 APK specifically for emulator testing +$ApkBasePath = $env:SENTRY_UNREAL_TEST_APP_PATH +if ($ApkBasePath -match "\.apk$") { + # Direct path to APK + $script:ApkPath = $ApkBasePath +} else { + # Path to directory containing APKs - pick x64 for emulator + $script:ApkPath = Join-Path $ApkBasePath "SentryPlayground-x64.apk" + if (-not (Test-Path $script:ApkPath)) { + throw "x64 APK not found at: $script:ApkPath" + } +} + +function Get-ApkPackageName([string]$ApkPath) { + $output = & aapt dump badging $ApkPath 2>&1 | Out-String + if ($output -match "package: name='([^']+)'") { + return $Matches[1] + } + throw "Could not extract package name from APK: $output" +} + +$script:PackageName = Get-ApkPackageName $script:ApkPath +$script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + +Write-Host "Package: $script:PackageName" -ForegroundColor Cyan +Write-Host "Activity: $script:ActivityName" -ForegroundColor Cyan +Write-Host "APK: $script:ApkPath" -ForegroundColor Cyan + +function Get-AndroidDeviceId { + $devices = adb devices | Select-String "device$" | ForEach-Object { ($_ -split '\s+')[0] } + if ($devices.Count -eq 0) { + throw "No Android devices found" + } + return $devices[0] +} + +function Invoke-AndroidTestApp { + param( + [Parameter(Mandatory)] + [string]$TestName, # 'crash-capture' or 'message-capture' + + [Parameter()] + [int]$TimeoutSeconds = 300, + + [Parameter()] + [switch]$SkipReinstall # Don't reinstall APK (for preserving crash state) + ) + + $device = Get-AndroidDeviceId + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $logFile = "$script:OutputDir/$timestamp-$TestName-logcat.txt" + + Write-Host "Running test: $TestName on device: $device" -ForegroundColor Yellow + + if (-not $SkipReinstall) { + # 1. Uninstall previous installation (to ensure clean state) + $installed = adb -s $device shell pm list packages | Select-String $script:PackageName + if ($installed) { + Write-Host "Uninstalling previous version..." + adb -s $device uninstall $script:PackageName | Out-Null + Start-Sleep -Seconds 1 + } + + # 2. Install APK + Write-Host "Installing APK..." + $installOutput = adb -s $device install -r $script:ApkPath 2>&1 | Out-String + if ($installOutput -notmatch "Success") { + throw "Failed to install APK: $installOutput" + } + } else { + Write-Host "Skipping reinstall (preserving crash state)..." -ForegroundColor Cyan + } + + # 3. Clear logcat + adb -s $device logcat -c + + # 4. Start activity with Intent extras + Write-Host "Starting activity with Intent extras: -e test $TestName" + $startOutput = adb -s $device shell am start -n $script:ActivityName -e test $TestName -W 2>&1 | Out-String + + if ($startOutput -match "Error") { + throw "Failed to start activity: $startOutput" + } + + # 5. Get process ID (with retries) + Write-Host "Waiting for app process..." + Start-Sleep -Seconds 3 + $appPID = $null + for ($i = 0; $i -lt 30; $i++) { + $pidOutput = (adb -s $device shell pidof $script:PackageName 2>&1).Trim() + if ($pidOutput -and $pidOutput -match '^\d+$') { + $appPID = $pidOutput + break + } + Start-Sleep -Seconds 1 + } + + if (-not $appPID) { + # App might have already exited (fast message test) - capture logs anyway + Write-Host "Warning: Could not find process ID (app may have exited quickly)" -ForegroundColor Yellow + $logCache = adb -s $device logcat -d 2>&1 + } else { + Write-Host "App PID: $appPID" -ForegroundColor Green + + # 6. Monitor logcat for test completion + $logCache = @() + $startTime = Get-Date + $completed = $false + + while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($TimeoutSeconds)) { + $newLogs = adb -s $device logcat -d --pid=$appPID 2>&1 + if ($newLogs) { + $logCache = $newLogs + + # Check for completion markers from SentryPlaygroundGameInstance + if (($newLogs | Select-String "TEST_RESULT:") -or + ($newLogs | Select-String "Requesting app exit")) { + $completed = $true + Write-Host "Test completion detected" -ForegroundColor Green + break + } + } + + Start-Sleep -Seconds 2 + } + + if (-not $completed) { + Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow + } + } + + # Save full logcat to file + $logCache | Out-File $logFile + Write-Host "Logcat saved to: $logFile" + + # 7. Return structured result + return @{ + ExitCode = if ($TestName -eq 'crash-capture') { -1 } else { 0 } # Simulate crash exit code + Output = $logCache + Error = @() + } +} + +BeforeAll { + # Check if configuration file exists + $configFile = "$PSScriptRoot/TestConfig.local.ps1" + if (-not (Test-Path $configFile)) { + throw "Configuration file '$configFile' not found. Run 'cmake -B build -S .' first" + } + + # Load configuration (provides $global:AppRunnerPath) + . $configFile + + # Import app-runner modules (SentryApiClient, test utilities) + . "$global:AppRunnerPath/import-modules.ps1" + + # Validate environment variables + $script:DSN = $env:SENTRY_UNREAL_TEST_DSN + $script:AuthToken = $env:SENTRY_AUTH_TOKEN + + if (-not $script:DSN) { + throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" + } + + if (-not $script:AuthToken) { + throw "Environment variable SENTRY_AUTH_TOKEN must be set" + } + + if (-not (Test-Path $script:ApkPath)) { + throw "APK not found at: $script:ApkPath" + } + + # Check adb and device + try { + $device = Get-AndroidDeviceId + Write-Host "Found Android device: $device" -ForegroundColor Green + } catch { + throw "No Android devices found. Is emulator running?" + } + + # Connect to Sentry API + Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow + Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken + + # Create output directory + $script:OutputDir = "$PSScriptRoot/output" + if (-not (Test-Path $script:OutputDir)) { + New-Item -ItemType Directory -Path $script:OutputDir | Out-Null + } + + # ========================================== + # RUN 1: Crash test - creates minidump + # ========================================== + # The crash is captured but NOT uploaded yet (Android behavior) + Write-Host "`n=== Running crash-capture test (will crash) ===" -ForegroundColor Yellow + $global:AndroidCrashResult = Invoke-AndroidTestApp -TestName 'crash-capture' + Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + + # ========================================== + # RUN 2: Message test - uploads crash from Run 1 + captures message + # ========================================== + # Android Sentry SDK uploads previous crash on next app start + # Use -SkipReinstall to preserve the crash state + Write-Host "`n=== Running message-capture test (will upload crash from previous run) ===" -ForegroundColor Yellow + $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' -SkipReinstall + Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan +} + +Describe "Sentry Unreal Android Integration Tests" { + + Context "Crash Capture Tests" { + BeforeAll { + # Crash event is sent during the MESSAGE run (Run 2) + # But the crash_id comes from the CRASH run (Run 1) + $script:CrashResult = $global:AndroidCrashResult + $script:CrashEvent = $null + + # Parse crash event ID from crash run output + $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + + if ($eventIds -and $eventIds.Count -gt 0) { + Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan + $crashId = $eventIds[0] + + # Fetch crash event using the tag (event was sent during message run) + try { + $script:CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green + } catch { + Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red + } + } else { + Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow + } + } + + It "Should output event ID before crash" { + $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + $eventIds | Should -Not -BeNullOrEmpty + $eventIds.Count | Should -Be 1 + } + + It "Should capture crash event in Sentry (uploaded during next run)" { + $script:CrashEvent | Should -Not -BeNullOrEmpty + } + + It "Should have correct event type and platform" { + $script:CrashEvent.type | Should -Be 'error' + $script:CrashEvent.platform | Should -Be 'native' + } + + It "Should have exception information" { + $script:CrashEvent.exception | Should -Not -BeNullOrEmpty + $script:CrashEvent.exception.values | Should -Not -BeNullOrEmpty + } + + It "Should have stack trace" { + $exception = $script:CrashEvent.exception.values[0] + $exception.stacktrace | Should -Not -BeNullOrEmpty + $exception.stacktrace.frames | Should -Not -BeNullOrEmpty + } + + It "Should have user context" { + $script:CrashEvent.user | Should -Not -BeNullOrEmpty + $script:CrashEvent.user.username | Should -Be 'TestUser' + $script:CrashEvent.user.email | Should -Be 'user-mail@test.abc' + $script:CrashEvent.user.id | Should -Be '12345' + } + + It "Should have test.crash_id tag for correlation" { + $tags = $script:CrashEvent.tags + $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } + $crashIdTag | Should -Not -BeNullOrEmpty + $crashIdTag.value | Should -Not -BeNullOrEmpty + } + + It "Should have integration test tag" { + $tags = $script:CrashEvent.tags + ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + } + + It "Should have breadcrumbs from before crash" { + $script:CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $script:CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } + + Context "Message Capture Tests" { + BeforeAll { + $script:MessageResult = $global:AndroidMessageResult + $script:MessageEvent = $null + + # Parse event ID from output + $eventIds = Get-EventIds -AppOutput $script:MessageResult.Output -ExpectedCount 1 + + if ($eventIds -and $eventIds.Count -gt 0) { + Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan + + # Fetch event from Sentry (with polling) + try { + $script:MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] + Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green + } catch { + Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red + } + } else { + Write-Host "Warning: No message event ID found in output" -ForegroundColor Yellow + } + } + + It "Should output event ID" { + $eventIds = Get-EventIds -AppOutput $script:MessageResult.Output -ExpectedCount 1 + $eventIds | Should -Not -BeNullOrEmpty + $eventIds.Count | Should -Be 1 + } + + It "Should output TEST_RESULT with success" { + $testResultLine = $script:MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine | Should -Not -BeNullOrEmpty + $testResultLine | Should -Match '"success"\s*:\s*true' + } + + It "Should capture message event in Sentry" { + $script:MessageEvent | Should -Not -BeNullOrEmpty + } + + It "Should have correct platform" { + $script:MessageEvent.platform | Should -Be 'native' + } + + It "Should have message content" { + $script:MessageEvent.message | Should -Not -BeNullOrEmpty + $script:MessageEvent.message.formatted | Should -Match 'Integration test message' + } + + It "Should have user context" { + $script:MessageEvent.user | Should -Not -BeNullOrEmpty + $script:MessageEvent.user.username | Should -Be 'TestUser' + } + + It "Should have integration test tag" { + $tags = $script:MessageEvent.tags + ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + } + + It "Should have breadcrumbs" { + $script:MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $script:MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } +} + +AfterAll { + Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow + Disconnect-SentryApi + Write-Host "Integration tests complete" -ForegroundColor Green +} \ No newline at end of file From 588aadac5b14cfc026ad16f100f6c21303fb90b1 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Nov 2025 16:35:43 +0200 Subject: [PATCH 03/52] Comment crash capturing test --- .../Integration.Tests.Android.ps1 | 184 ++++++++++-------- 1 file changed, 98 insertions(+), 86 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index e794005f5..027dd83ca 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -198,101 +198,113 @@ BeforeAll { } # ========================================== - # RUN 1: Crash test - creates minidump + # NOTE: Crash test is currently DISABLED due to tag sync issue + # The test.crash_id tag set before crash is not synced to the captured event on Android + # TODO: Re-enable once Android SDK tag persistence is fixed # ========================================== - # The crash is captured but NOT uploaded yet (Android behavior) - Write-Host "`n=== Running crash-capture test (will crash) ===" -ForegroundColor Yellow - $global:AndroidCrashResult = Invoke-AndroidTestApp -TestName 'crash-capture' - Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + # # RUN 1: Crash test - creates minidump + # # The crash is captured but NOT uploaded yet (Android behavior) + # Write-Host "`n=== Running crash-capture test (will crash) ===" -ForegroundColor Yellow + # $global:AndroidCrashResult = Invoke-AndroidTestApp -TestName 'crash-capture' + # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + # + # # RUN 2: Message test - uploads crash from Run 1 + captures message + # # Android Sentry SDK uploads previous crash on next app start + # # Use -SkipReinstall to preserve the crash state + # Write-Host "`n=== Running message-capture test (will upload crash from previous run) ===" -ForegroundColor Yellow + # $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' -SkipReinstall + # Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan # ========================================== - # RUN 2: Message test - uploads crash from Run 1 + captures message + # RUN: Message test only (crash test disabled) # ========================================== - # Android Sentry SDK uploads previous crash on next app start - # Use -SkipReinstall to preserve the crash state - Write-Host "`n=== Running message-capture test (will upload crash from previous run) ===" -ForegroundColor Yellow - $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' -SkipReinstall + Write-Host "`n=== Running message-capture test ===" -ForegroundColor Yellow + $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan } Describe "Sentry Unreal Android Integration Tests" { - Context "Crash Capture Tests" { - BeforeAll { - # Crash event is sent during the MESSAGE run (Run 2) - # But the crash_id comes from the CRASH run (Run 1) - $script:CrashResult = $global:AndroidCrashResult - $script:CrashEvent = $null - - # Parse crash event ID from crash run output - $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 - - if ($eventIds -and $eventIds.Count -gt 0) { - Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan - $crashId = $eventIds[0] - - # Fetch crash event using the tag (event was sent during message run) - try { - $script:CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" - Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green - } catch { - Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red - } - } else { - Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow - } - } - - It "Should output event ID before crash" { - $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 - $eventIds | Should -Not -BeNullOrEmpty - $eventIds.Count | Should -Be 1 - } - - It "Should capture crash event in Sentry (uploaded during next run)" { - $script:CrashEvent | Should -Not -BeNullOrEmpty - } - - It "Should have correct event type and platform" { - $script:CrashEvent.type | Should -Be 'error' - $script:CrashEvent.platform | Should -Be 'native' - } - - It "Should have exception information" { - $script:CrashEvent.exception | Should -Not -BeNullOrEmpty - $script:CrashEvent.exception.values | Should -Not -BeNullOrEmpty - } - - It "Should have stack trace" { - $exception = $script:CrashEvent.exception.values[0] - $exception.stacktrace | Should -Not -BeNullOrEmpty - $exception.stacktrace.frames | Should -Not -BeNullOrEmpty - } - - It "Should have user context" { - $script:CrashEvent.user | Should -Not -BeNullOrEmpty - $script:CrashEvent.user.username | Should -Be 'TestUser' - $script:CrashEvent.user.email | Should -Be 'user-mail@test.abc' - $script:CrashEvent.user.id | Should -Be '12345' - } - - It "Should have test.crash_id tag for correlation" { - $tags = $script:CrashEvent.tags - $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } - $crashIdTag | Should -Not -BeNullOrEmpty - $crashIdTag.value | Should -Not -BeNullOrEmpty - } - - It "Should have integration test tag" { - $tags = $script:CrashEvent.tags - ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - } - - It "Should have breadcrumbs from before crash" { - $script:CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $script:CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - } - } + # ========================================== + # NOTE: Crash Capture Tests are DISABLED due to tag sync issue + # Uncomment when Android SDK tag persistence is fixed + # ========================================== + # Context "Crash Capture Tests" { + # BeforeAll { + # # Crash event is sent during the MESSAGE run (Run 2) + # # But the crash_id comes from the CRASH run (Run 1) + # $script:CrashResult = $global:AndroidCrashResult + # $script:CrashEvent = $null + # + # # Parse crash event ID from crash run output + # $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + # + # if ($eventIds -and $eventIds.Count -gt 0) { + # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan + # $crashId = $eventIds[0] + # + # # Fetch crash event using the tag (event was sent during message run) + # try { + # $script:CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green + # } catch { + # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red + # } + # } else { + # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow + # } + # } + # + # It "Should output event ID before crash" { + # $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + # $eventIds | Should -Not -BeNullOrEmpty + # $eventIds.Count | Should -Be 1 + # } + # + # It "Should capture crash event in Sentry (uploaded during next run)" { + # $script:CrashEvent | Should -Not -BeNullOrEmpty + # } + # + # It "Should have correct event type and platform" { + # $script:CrashEvent.type | Should -Be 'error' + # $script:CrashEvent.platform | Should -Be 'native' + # } + # + # It "Should have exception information" { + # $script:CrashEvent.exception | Should -Not -BeNullOrEmpty + # $script:CrashEvent.exception.values | Should -Not -BeNullOrEmpty + # } + # + # It "Should have stack trace" { + # $exception = $script:CrashEvent.exception.values[0] + # $exception.stacktrace | Should -Not -BeNullOrEmpty + # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty + # } + # + # It "Should have user context" { + # $script:CrashEvent.user | Should -Not -BeNullOrEmpty + # $script:CrashEvent.user.username | Should -Be 'TestUser' + # $script:CrashEvent.user.email | Should -Be 'user-mail@test.abc' + # $script:CrashEvent.user.id | Should -Be '12345' + # } + # + # It "Should have test.crash_id tag for correlation" { + # $tags = $script:CrashEvent.tags + # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } + # $crashIdTag | Should -Not -BeNullOrEmpty + # $crashIdTag.value | Should -Not -BeNullOrEmpty + # } + # + # It "Should have integration test tag" { + # $tags = $script:CrashEvent.tags + # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + # } + # + # It "Should have breadcrumbs from before crash" { + # $script:CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + # $script:CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + # } + # } Context "Message Capture Tests" { BeforeAll { From c82a55ea168062dc2fde2bb11f848fe66b0fcf59 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 09:34:14 +0200 Subject: [PATCH 04/52] Fix test script --- .../Integration.Tests.Android.ps1 | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 027dd83ca..205e8e2e7 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -7,43 +7,27 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -# Get the x64 APK specifically for emulator testing -$ApkBasePath = $env:SENTRY_UNREAL_TEST_APP_PATH -if ($ApkBasePath -match "\.apk$") { - # Direct path to APK - $script:ApkPath = $ApkBasePath -} else { - # Path to directory containing APKs - pick x64 for emulator - $script:ApkPath = Join-Path $ApkBasePath "SentryPlayground-x64.apk" - if (-not (Test-Path $script:ApkPath)) { - throw "x64 APK not found at: $script:ApkPath" - } -} +function script:Get-AndroidDeviceId { + # Get lines that end with "device" (not "offline" or "unauthorized") + $lines = adb devices | Select-String "device$" -function Get-ApkPackageName([string]$ApkPath) { - $output = & aapt dump badging $ApkPath 2>&1 | Out-String - if ($output -match "package: name='([^']+)'") { - return $Matches[1] + if (-not $lines) { + throw "No Android devices found" } - throw "Could not extract package name from APK: $output" -} - -$script:PackageName = Get-ApkPackageName $script:ApkPath -$script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" -Write-Host "Package: $script:PackageName" -ForegroundColor Cyan -Write-Host "Activity: $script:ActivityName" -ForegroundColor Cyan -Write-Host "APK: $script:ApkPath" -ForegroundColor Cyan + # Extract device ID from the first matching line + # Line format: "emulator-5554 device" + $firstLine = $lines | Select-Object -First 1 + $deviceId = ($firstLine.Line -split '\s+')[0] -function Get-AndroidDeviceId { - $devices = adb devices | Select-String "device$" | ForEach-Object { ($_ -split '\s+')[0] } - if ($devices.Count -eq 0) { - throw "No Android devices found" + if (-not $deviceId) { + throw "Could not extract device ID from: $($firstLine.Line)" } - return $devices[0] + + return $deviceId } -function Invoke-AndroidTestApp { +function script:Invoke-AndroidTestApp { param( [Parameter(Mandatory)] [string]$TestName, # 'crash-capture' or 'message-capture' @@ -63,10 +47,11 @@ function Invoke-AndroidTestApp { if (-not $SkipReinstall) { # 1. Uninstall previous installation (to ensure clean state) - $installed = adb -s $device shell pm list packages | Select-String $script:PackageName + $packageName = $script:PackageName + $installed = adb -s $device shell pm list packages | Select-String -Pattern $packageName -SimpleMatch if ($installed) { Write-Host "Uninstalling previous version..." - adb -s $device uninstall $script:PackageName | Out-Null + adb -s $device uninstall $packageName | Out-Null Start-Sleep -Seconds 1 } @@ -95,11 +80,15 @@ function Invoke-AndroidTestApp { Write-Host "Waiting for app process..." Start-Sleep -Seconds 3 $appPID = $null + $packageName = $script:PackageName for ($i = 0; $i -lt 30; $i++) { - $pidOutput = (adb -s $device shell pidof $script:PackageName 2>&1).Trim() - if ($pidOutput -and $pidOutput -match '^\d+$') { - $appPID = $pidOutput - break + $pidOutput = adb -s $device shell pidof $packageName 2>&1 + if ($pidOutput) { + $pidOutput = $pidOutput.ToString().Trim() + if ($pidOutput -match '^\d+$') { + $appPID = $pidOutput + break + } } Start-Sleep -Seconds 1 } @@ -151,6 +140,11 @@ function Invoke-AndroidTestApp { } BeforeAll { + # Package name is defined in sample/Config/DefaultEngine.ini + # If this changes, update this constant + $script:PackageName = "io.sentry.unreal.sample" + $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + # Check if configuration file exists $configFile = "$PSScriptRoot/TestConfig.local.ps1" if (-not (Test-Path $configFile)) { @@ -175,10 +169,26 @@ BeforeAll { throw "Environment variable SENTRY_AUTH_TOKEN must be set" } + # Resolve APK path from environment variable + $ApkBasePath = $env:SENTRY_UNREAL_TEST_APP_PATH + if (-not $ApkBasePath) { + throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" + } + + if ($ApkBasePath -match "\.apk$") { + # Direct path to APK + $script:ApkPath = $ApkBasePath + } else { + # Path to directory containing APKs - pick x64 for emulator + $script:ApkPath = Join-Path $ApkBasePath "SentryPlayground-x64.apk" + } + if (-not (Test-Path $script:ApkPath)) { throw "APK not found at: $script:ApkPath" } + Write-Host "APK: $script:ApkPath" -ForegroundColor Cyan + # Check adb and device try { $device = Get-AndroidDeviceId @@ -346,7 +356,8 @@ Describe "Sentry Unreal Android Integration Tests" { } It "Should have correct platform" { - $script:MessageEvent.platform | Should -Be 'native' + # Android events are captured from Java layer, so platform is 'java' not 'native' + $script:MessageEvent.platform | Should -Be 'java' } It "Should have message content" { From 6b14d0ab4ac2be87dc5c420ec22481891f02a327 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 10:01:06 +0200 Subject: [PATCH 05/52] Fix minor issues --- .../Integration.Tests.Android.ps1 | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 205e8e2e7..f16245bf9 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -27,6 +27,28 @@ function script:Get-AndroidDeviceId { return $deviceId } +function script:Get-PackageNameFromApk { + param( + [Parameter(Mandatory)] + [string]$ApkPath + ) + + # Use aapt to extract package name from APK + # aapt dump badging returns: package: name='io.sentry.unreal.sample' versionCode='1' ... + $aaptOutput = aapt dump badging "$ApkPath" 2>&1 | Select-String "^package:" + + if (-not $aaptOutput) { + # Fallback: try using adb directly + $aaptOutput = adb shell dumpsys package "$ApkPath" 2>&1 | Select-String "package:" + } + + if ($aaptOutput -and $aaptOutput.Line -match "name='([^']+)'") { + return $matches[1] + } + + throw "Could not extract package name from APK: $ApkPath" +} + function script:Invoke-AndroidTestApp { param( [Parameter(Mandatory)] @@ -36,7 +58,16 @@ function script:Invoke-AndroidTestApp { [int]$TimeoutSeconds = 300, [Parameter()] - [switch]$SkipReinstall # Don't reinstall APK (for preserving crash state) + [switch]$SkipReinstall, # Don't reinstall APK (for preserving crash state) + + [Parameter()] + [int]$InitialWaitSeconds = 3, # Wait time before checking for PID + + [Parameter()] + [int]$PidRetrySeconds = 30, # Timeout for PID detection (in seconds) + + [Parameter()] + [int]$LogPollIntervalSeconds = 2 # Logcat polling interval ) $device = Get-AndroidDeviceId @@ -58,8 +89,8 @@ function script:Invoke-AndroidTestApp { # 2. Install APK Write-Host "Installing APK..." $installOutput = adb -s $device install -r $script:ApkPath 2>&1 | Out-String - if ($installOutput -notmatch "Success") { - throw "Failed to install APK: $installOutput" + if ($LASTEXITCODE -ne 0 -or $installOutput -notmatch "Success") { + throw "Failed to install APK (exit code: $LASTEXITCODE): $installOutput" } } else { Write-Host "Skipping reinstall (preserving crash state)..." -ForegroundColor Cyan @@ -78,10 +109,10 @@ function script:Invoke-AndroidTestApp { # 5. Get process ID (with retries) Write-Host "Waiting for app process..." - Start-Sleep -Seconds 3 + Start-Sleep -Seconds $InitialWaitSeconds $appPID = $null $packageName = $script:PackageName - for ($i = 0; $i -lt 30; $i++) { + for ($i = 0; $i -lt $PidRetrySeconds; $i++) { $pidOutput = adb -s $device shell pidof $packageName 2>&1 if ($pidOutput) { $pidOutput = $pidOutput.ToString().Trim() @@ -93,22 +124,24 @@ function script:Invoke-AndroidTestApp { Start-Sleep -Seconds 1 } + # Initialize logCache as array for consistent type handling + [array]$logCache = @() + if (-not $appPID) { # App might have already exited (fast message test) - capture logs anyway Write-Host "Warning: Could not find process ID (app may have exited quickly)" -ForegroundColor Yellow - $logCache = adb -s $device logcat -d 2>&1 + $logCache = @(adb -s $device logcat -d 2>&1) } else { Write-Host "App PID: $appPID" -ForegroundColor Green # 6. Monitor logcat for test completion - $logCache = @() $startTime = Get-Date $completed = $false while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($TimeoutSeconds)) { $newLogs = adb -s $device logcat -d --pid=$appPID 2>&1 if ($newLogs) { - $logCache = $newLogs + $logCache = @($newLogs) # Check for completion markers from SentryPlaygroundGameInstance if (($newLogs | Select-String "TEST_RESULT:") -or @@ -119,7 +152,7 @@ function script:Invoke-AndroidTestApp { } } - Start-Sleep -Seconds 2 + Start-Sleep -Seconds $LogPollIntervalSeconds } if (-not $completed) { @@ -140,11 +173,6 @@ function script:Invoke-AndroidTestApp { } BeforeAll { - # Package name is defined in sample/Config/DefaultEngine.ini - # If this changes, update this constant - $script:PackageName = "io.sentry.unreal.sample" - $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - # Check if configuration file exists $configFile = "$PSScriptRoot/TestConfig.local.ps1" if (-not (Test-Path $configFile)) { @@ -189,6 +217,16 @@ BeforeAll { Write-Host "APK: $script:ApkPath" -ForegroundColor Cyan + # Extract package name from APK (prevents hardcoded drift) + try { + $script:PackageName = Get-PackageNameFromApk -ApkPath $script:ApkPath + Write-Host "Package name: $script:PackageName" -ForegroundColor Cyan + } catch { + Write-Host "Warning: Could not extract package name from APK, using default" -ForegroundColor Yellow + $script:PackageName = "io.sentry.unreal.sample" + } + $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + # Check adb and device try { $device = Get-AndroidDeviceId From a7bb5e3e370ee57016ce17ab698c2537eea3b28d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 10:05:48 +0200 Subject: [PATCH 06/52] Rename vars --- .../Integration.Tests.Android.ps1 | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index f16245bf9..21839915a 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -281,11 +281,11 @@ Describe "Sentry Unreal Android Integration Tests" { # BeforeAll { # # Crash event is sent during the MESSAGE run (Run 2) # # But the crash_id comes from the CRASH run (Run 1) - # $script:CrashResult = $global:AndroidCrashResult - # $script:CrashEvent = $null + # $CrashResult = $global:AndroidCrashResult + # $CrashEvent = $null # # # Parse crash event ID from crash run output - # $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 # # if ($eventIds -and $eventIds.Count -gt 0) { # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan @@ -293,7 +293,7 @@ Describe "Sentry Unreal Android Integration Tests" { # # # Fetch crash event using the tag (event was sent during message run) # try { - # $script:CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green # } catch { # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red @@ -304,70 +304,70 @@ Describe "Sentry Unreal Android Integration Tests" { # } # # It "Should output event ID before crash" { - # $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 # $eventIds | Should -Not -BeNullOrEmpty # $eventIds.Count | Should -Be 1 # } # # It "Should capture crash event in Sentry (uploaded during next run)" { - # $script:CrashEvent | Should -Not -BeNullOrEmpty + # $CrashEvent | Should -Not -BeNullOrEmpty # } # # It "Should have correct event type and platform" { - # $script:CrashEvent.type | Should -Be 'error' - # $script:CrashEvent.platform | Should -Be 'native' + # $CrashEvent.type | Should -Be 'error' + # $CrashEvent.platform | Should -Be 'native' # } # # It "Should have exception information" { - # $script:CrashEvent.exception | Should -Not -BeNullOrEmpty - # $script:CrashEvent.exception.values | Should -Not -BeNullOrEmpty + # $CrashEvent.exception | Should -Not -BeNullOrEmpty + # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty # } # # It "Should have stack trace" { - # $exception = $script:CrashEvent.exception.values[0] + # $exception = $CrashEvent.exception.values[0] # $exception.stacktrace | Should -Not -BeNullOrEmpty # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty # } # # It "Should have user context" { - # $script:CrashEvent.user | Should -Not -BeNullOrEmpty - # $script:CrashEvent.user.username | Should -Be 'TestUser' - # $script:CrashEvent.user.email | Should -Be 'user-mail@test.abc' - # $script:CrashEvent.user.id | Should -Be '12345' + # $CrashEvent.user | Should -Not -BeNullOrEmpty + # $CrashEvent.user.username | Should -Be 'TestUser' + # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' + # $CrashEvent.user.id | Should -Be '12345' # } # # It "Should have test.crash_id tag for correlation" { - # $tags = $script:CrashEvent.tags + # $tags = $CrashEvent.tags # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } # $crashIdTag | Should -Not -BeNullOrEmpty # $crashIdTag.value | Should -Not -BeNullOrEmpty # } # # It "Should have integration test tag" { - # $tags = $script:CrashEvent.tags + # $tags = $CrashEvent.tags # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' # } # # It "Should have breadcrumbs from before crash" { - # $script:CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - # $script:CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty # } # } Context "Message Capture Tests" { BeforeAll { - $script:MessageResult = $global:AndroidMessageResult - $script:MessageEvent = $null + $MessageResult = $global:AndroidMessageResult + $MessageEvent = $null # Parse event ID from output - $eventIds = Get-EventIds -AppOutput $script:MessageResult.Output -ExpectedCount 1 + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 if ($eventIds -and $eventIds.Count -gt 0) { Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan # Fetch event from Sentry (with polling) try { - $script:MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] + $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green } catch { Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red @@ -378,44 +378,44 @@ Describe "Sentry Unreal Android Integration Tests" { } It "Should output event ID" { - $eventIds = Get-EventIds -AppOutput $script:MessageResult.Output -ExpectedCount 1 + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 $eventIds | Should -Not -BeNullOrEmpty $eventIds.Count | Should -Be 1 } It "Should output TEST_RESULT with success" { - $testResultLine = $script:MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine = $MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } $testResultLine | Should -Not -BeNullOrEmpty $testResultLine | Should -Match '"success"\s*:\s*true' } It "Should capture message event in Sentry" { - $script:MessageEvent | Should -Not -BeNullOrEmpty + $MessageEvent | Should -Not -BeNullOrEmpty } It "Should have correct platform" { # Android events are captured from Java layer, so platform is 'java' not 'native' - $script:MessageEvent.platform | Should -Be 'java' + $MessageEvent.platform | Should -Be 'java' } It "Should have message content" { - $script:MessageEvent.message | Should -Not -BeNullOrEmpty - $script:MessageEvent.message.formatted | Should -Match 'Integration test message' + $MessageEvent.message | Should -Not -BeNullOrEmpty + $MessageEvent.message.formatted | Should -Match 'Integration test message' } It "Should have user context" { - $script:MessageEvent.user | Should -Not -BeNullOrEmpty - $script:MessageEvent.user.username | Should -Be 'TestUser' + $MessageEvent.user | Should -Not -BeNullOrEmpty + $MessageEvent.user.username | Should -Be 'TestUser' } It "Should have integration test tag" { - $tags = $script:MessageEvent.tags + $tags = $MessageEvent.tags ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' } It "Should have breadcrumbs" { - $script:MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $script:MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty } } } From 7b3ab686a04e118dd2bd904d1b5db41987525c4d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 10:50:35 +0200 Subject: [PATCH 07/52] Clean up --- .../workflows/integration-test-android.yml | 2 +- .../Integration.Tests.Android.ps1 | 67 +++---------------- 2 files changed, 11 insertions(+), 58 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 07ee2ccdf..b1c0dcd9e 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -75,7 +75,7 @@ jobs: env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} - SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build + SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-x64.apk - name: Upload integration test output if: ${{ always() && steps.integration-test.outcome == 'failure' }} diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 21839915a..f77b9a64b 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -27,28 +27,6 @@ function script:Get-AndroidDeviceId { return $deviceId } -function script:Get-PackageNameFromApk { - param( - [Parameter(Mandatory)] - [string]$ApkPath - ) - - # Use aapt to extract package name from APK - # aapt dump badging returns: package: name='io.sentry.unreal.sample' versionCode='1' ... - $aaptOutput = aapt dump badging "$ApkPath" 2>&1 | Select-String "^package:" - - if (-not $aaptOutput) { - # Fallback: try using adb directly - $aaptOutput = adb shell dumpsys package "$ApkPath" 2>&1 | Select-String "package:" - } - - if ($aaptOutput -and $aaptOutput.Line -match "name='([^']+)'") { - return $matches[1] - } - - throw "Could not extract package name from APK: $ApkPath" -} - function script:Invoke-AndroidTestApp { param( [Parameter(Mandatory)] @@ -188,6 +166,7 @@ BeforeAll { # Validate environment variables $script:DSN = $env:SENTRY_UNREAL_TEST_DSN $script:AuthToken = $env:SENTRY_AUTH_TOKEN + $script:ApkPath = $env:SENTRY_UNREAL_TEST_APP_PATH if (-not $script:DSN) { throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" @@ -197,54 +176,28 @@ BeforeAll { throw "Environment variable SENTRY_AUTH_TOKEN must be set" } - # Resolve APK path from environment variable - $ApkBasePath = $env:SENTRY_UNREAL_TEST_APP_PATH - if (-not $ApkBasePath) { + if (-not $script:ApkPath) { throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" } - if ($ApkBasePath -match "\.apk$") { - # Direct path to APK - $script:ApkPath = $ApkBasePath - } else { - # Path to directory containing APKs - pick x64 for emulator - $script:ApkPath = Join-Path $ApkBasePath "SentryPlayground-x64.apk" - } - - if (-not (Test-Path $script:ApkPath)) { - throw "APK not found at: $script:ApkPath" - } - - Write-Host "APK: $script:ApkPath" -ForegroundColor Cyan - - # Extract package name from APK (prevents hardcoded drift) - try { - $script:PackageName = Get-PackageNameFromApk -ApkPath $script:ApkPath - Write-Host "Package name: $script:PackageName" -ForegroundColor Cyan - } catch { - Write-Host "Warning: Could not extract package name from APK, using default" -ForegroundColor Yellow - $script:PackageName = "io.sentry.unreal.sample" - } - $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - - # Check adb and device - try { - $device = Get-AndroidDeviceId - Write-Host "Found Android device: $device" -ForegroundColor Green - } catch { - throw "No Android devices found. Is emulator running?" - } - # Connect to Sentry API Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken + # Validate app path + if (-not (Test-Path $script:ApkPath)) { + throw "Application not found at: $script:ApkPath" + } + # Create output directory $script:OutputDir = "$PSScriptRoot/output" if (-not (Test-Path $script:OutputDir)) { New-Item -ItemType Directory -Path $script:OutputDir | Out-Null } + $script:PackageName = "io.sentry.unreal.sample" + $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + # ========================================== # NOTE: Crash test is currently DISABLED due to tag sync issue # The test.crash_id tag set before crash is not synced to the captured event on Android From a47a7ea20d003cfecef8ac384cec7a54ac20caa7 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 11:01:03 +0200 Subject: [PATCH 08/52] Better comments --- .../Integration.Tests.Android.ps1 | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index f77b9a64b..02b7e34c0 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -199,28 +199,25 @@ BeforeAll { $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" # ========================================== - # NOTE: Crash test is currently DISABLED due to tag sync issue - # The test.crash_id tag set before crash is not synced to the captured event on Android - # TODO: Re-enable once Android SDK tag persistence is fixed + # RUN 1: Crash test - creates minidump # ========================================== - # # RUN 1: Crash test - creates minidump - # # The crash is captured but NOT uploaded yet (Android behavior) - # Write-Host "`n=== Running crash-capture test (will crash) ===" -ForegroundColor Yellow + # The crash is captured but NOT uploaded yet (Android behavior). + # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) + + # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow # $global:AndroidCrashResult = Invoke-AndroidTestApp -TestName 'crash-capture' + # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan - # - # # RUN 2: Message test - uploads crash from Run 1 + captures message - # # Android Sentry SDK uploads previous crash on next app start - # # Use -SkipReinstall to preserve the crash state - # Write-Host "`n=== Running message-capture test (will upload crash from previous run) ===" -ForegroundColor Yellow - # $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' -SkipReinstall - # Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan # ========================================== - # RUN: Message test only (crash test disabled) + # RUN 2: Message test - uploads crash from Run 1 + captures message # ========================================== - Write-Host "`n=== Running message-capture test ===" -ForegroundColor Yellow + # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. + # TODO: use -SkipReinstall to preserve the crash state. + + Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' + Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan } From 35c488bec1d17f64f1d9512907e862d7aa978d02 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 12:00:44 +0200 Subject: [PATCH 09/52] Refactor --- .../Integration.Tests.Android.ps1 | 160 +++++++++++------- 1 file changed, 100 insertions(+), 60 deletions(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 02b7e34c0..013970b3f 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -8,11 +8,10 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function script:Get-AndroidDeviceId { - # Get lines that end with "device" (not "offline" or "unauthorized") $lines = adb devices | Select-String "device$" if (-not $lines) { - throw "No Android devices found" + throw "No Android devices found. Is emulator running?" } # Extract device ID from the first matching line @@ -27,71 +26,98 @@ function script:Get-AndroidDeviceId { return $deviceId } -function script:Invoke-AndroidTestApp { +function script:Install-AndroidApp { param( [Parameter(Mandatory)] - [string]$TestName, # 'crash-capture' or 'message-capture' + [string]$ApkPath, - [Parameter()] - [int]$TimeoutSeconds = 300, + [Parameter(Mandatory)] + [string]$PackageName, - [Parameter()] - [switch]$SkipReinstall, # Don't reinstall APK (for preserving crash state) + [Parameter(Mandatory)] + [string]$DeviceId + ) - [Parameter()] - [int]$InitialWaitSeconds = 3, # Wait time before checking for PID + if (-not (Test-Path $ApkPath)) { + throw "APK file not found: $ApkPath" + } - [Parameter()] - [int]$PidRetrySeconds = 30, # Timeout for PID detection (in seconds) + if ($ApkPath -notlike '*.apk') { + throw "Package must be an .apk file. Got: $ApkPath" + } + + # Check for existing installation + Write-Debug "Checking for existing package: $PackageName" + $installed = adb -s $DeviceId shell pm list packages | Select-String -Pattern $PackageName -SimpleMatch + + if ($installed) { + Write-Host "Uninstalling previous version..." -ForegroundColor Yellow + adb -s $DeviceId uninstall $PackageName | Out-Null + Start-Sleep -Seconds 1 + } + + # Install APK + Write-Host "Installing APK to device: $DeviceId" -ForegroundColor Yellow + $installOutput = adb -s $DeviceId install -r $ApkPath 2>&1 | Out-String + + if ($LASTEXITCODE -ne 0 -or $installOutput -notmatch "Success") { + throw "Failed to install APK (exit code: $LASTEXITCODE): $installOutput" + } + + Write-Host "Package installed successfully: $PackageName" -ForegroundColor Green +} + +function script:Invoke-AndroidApp { + param( + [Parameter(Mandatory)] + [string]$ExecutablePath, [Parameter()] - [int]$LogPollIntervalSeconds = 2 # Logcat polling interval + [string]$Arguments = "" ) - $device = Get-AndroidDeviceId + # Extract package name from activity path (format: package.name/activity.name) + if ($ExecutablePath -notmatch '^([^/]+)/') { + throw "ExecutablePath must be in format 'package.name/activity.name'. Got: $ExecutablePath" + } + $packageName = $matches[1] + + # Use script-level Android configuration + $deviceId = $script:DeviceId + $outputDir = $script:OutputDir + + # Android-specific timeout configuration + $timeoutSeconds = 300 + $initialWaitSeconds = 3 + $pidRetrySeconds = 30 + $logPollIntervalSeconds = 2 + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $logFile = "$script:OutputDir/$timestamp-$TestName-logcat.txt" - - Write-Host "Running test: $TestName on device: $device" -ForegroundColor Yellow - - if (-not $SkipReinstall) { - # 1. Uninstall previous installation (to ensure clean state) - $packageName = $script:PackageName - $installed = adb -s $device shell pm list packages | Select-String -Pattern $packageName -SimpleMatch - if ($installed) { - Write-Host "Uninstalling previous version..." - adb -s $device uninstall $packageName | Out-Null - Start-Sleep -Seconds 1 - } + $logFile = if ($OutputDir) { "$OutputDir/$timestamp-logcat.txt" } else { $null } - # 2. Install APK - Write-Host "Installing APK..." - $installOutput = adb -s $device install -r $script:ApkPath 2>&1 | Out-String - if ($LASTEXITCODE -ne 0 -or $installOutput -notmatch "Success") { - throw "Failed to install APK (exit code: $LASTEXITCODE): $installOutput" - } - } else { - Write-Host "Skipping reinstall (preserving crash state)..." -ForegroundColor Cyan - } + # Clear logcat before launch + Write-Debug "Clearing logcat on device: $deviceId" + adb -s $deviceId logcat -c - # 3. Clear logcat - adb -s $device logcat -c + # Launch activity with Intent extras + Write-Host "Launching: $ExecutablePath" -ForegroundColor Cyan + if ($Arguments) { + Write-Host " Arguments: $Arguments" -ForegroundColor Cyan + } - # 4. Start activity with Intent extras - Write-Host "Starting activity with Intent extras: -e test $TestName" - $startOutput = adb -s $device shell am start -n $script:ActivityName -e test $TestName -W 2>&1 | Out-String + $startOutput = adb -s $deviceId shell am start -n $ExecutablePath $Arguments -W 2>&1 | Out-String if ($startOutput -match "Error") { throw "Failed to start activity: $startOutput" } - # 5. Get process ID (with retries) - Write-Host "Waiting for app process..." - Start-Sleep -Seconds $InitialWaitSeconds + # Get process ID (with retries) + Write-Debug "Waiting for app process..." + Start-Sleep -Seconds $initialWaitSeconds + $appPID = $null - $packageName = $script:PackageName - for ($i = 0; $i -lt $PidRetrySeconds; $i++) { - $pidOutput = adb -s $device shell pidof $packageName 2>&1 + for ($i = 0; $i -lt $pidRetrySeconds; $i++) { + $pidOutput = adb -s $deviceId shell pidof $packageName 2>&1 if ($pidOutput) { $pidOutput = $pidOutput.ToString().Trim() if ($pidOutput -match '^\d+$') { @@ -102,22 +128,23 @@ function script:Invoke-AndroidTestApp { Start-Sleep -Seconds 1 } - # Initialize logCache as array for consistent type handling + # Initialize log cache as array for consistent type handling [array]$logCache = @() if (-not $appPID) { # App might have already exited (fast message test) - capture logs anyway Write-Host "Warning: Could not find process ID (app may have exited quickly)" -ForegroundColor Yellow - $logCache = @(adb -s $device logcat -d 2>&1) + $logCache = @(adb -s $deviceId logcat -d 2>&1) + $exitCode = 0 } else { Write-Host "App PID: $appPID" -ForegroundColor Green - # 6. Monitor logcat for test completion + # Monitor logcat for test completion $startTime = Get-Date $completed = $false - while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($TimeoutSeconds)) { - $newLogs = adb -s $device logcat -d --pid=$appPID 2>&1 + while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($timeoutSeconds)) { + $newLogs = adb -s $deviceId logcat -d --pid=$appPID 2>&1 if ($newLogs) { $logCache = @($newLogs) @@ -130,21 +157,25 @@ function script:Invoke-AndroidTestApp { } } - Start-Sleep -Seconds $LogPollIntervalSeconds + Start-Sleep -Seconds $logPollIntervalSeconds } if (-not $completed) { Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow } + + $exitCode = 0 # Android apps don't report exit codes via adb } - # Save full logcat to file - $logCache | Out-File $logFile - Write-Host "Logcat saved to: $logFile" + # Save logcat to file if OutputDir specified + if ($logFile) { + $logCache | Out-File $logFile + Write-Host "Logcat saved to: $logFile" + } - # 7. Return structured result + # Return structured result (matches app-runner pattern) return @{ - ExitCode = if ($TestName -eq 'crash-capture') { -1 } else { 0 } # Simulate crash exit code + ExitCode = $exitCode Output = $logCache Error = @() } @@ -198,6 +229,14 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + # Get Android device + $script:DeviceId = Get-AndroidDeviceId + Write-Host "Found Android device: $script:DeviceId" -ForegroundColor Green + + # Install APK to device + Write-Host "Installing APK to Android device..." -ForegroundColor Yellow + Install-AndroidApp -ApkPath $script:ApkPath -PackageName $script:PackageName -DeviceId $script:DeviceId + # ========================================== # RUN 1: Crash test - creates minidump # ========================================== @@ -205,7 +244,7 @@ BeforeAll { # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $global:AndroidCrashResult = Invoke-AndroidTestApp -TestName 'crash-capture' + # $global:AndroidCrashResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments "-e test crash-capture" # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan @@ -216,7 +255,8 @@ BeforeAll { # TODO: use -SkipReinstall to preserve the crash state. Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - $global:AndroidMessageResult = Invoke-AndroidTestApp -TestName 'message-capture' + # TODO: When AndroidProvider is added to app-runner: Invoke-DeviceApp $script:ActivityName -Arguments "-e test message-capture" + $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments "-e test message-capture" Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan } From 297cf3687e96b8bb83c0c0ab45fe61f78c83681b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 12:04:52 +0200 Subject: [PATCH 10/52] Fix workflow --- .github/workflows/integration-test-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index b1c0dcd9e..8bddd638c 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -50,7 +50,7 @@ jobs: cmake -B build -S . - name: Run Android Integration Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b id: integration-test timeout-minutes: 45 with: From d62218d15e7082e3d32723416c3644afdf05cc7d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 12 Nov 2025 13:22:36 +0200 Subject: [PATCH 11/52] Fix --- .github/workflows/integration-test-android.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 8bddd638c..4a38a0146 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -70,8 +70,7 @@ jobs: -camera-front none script: | adb wait-for-device - cd integration-test - pwsh -Command "Invoke-Pester Integration.Tests.Android.ps1 -CI" + cd integration-test && pwsh -Command "Invoke-Pester Integration.Tests.Android.ps1 -CI" env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} From 35862ce1bd8fb62b5cee6963f0f02a261e6909cc Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 13 Nov 2025 09:27:07 +0200 Subject: [PATCH 12/52] Simplify mechanism to pass input args to Android app --- .../Integration.Tests.Android.ps1 | 6 +- .../SentryPlayground.Build.cs | 7 --- .../SentryPlaygroundGameInstance.cpp | 62 +------------------ .../SentryPlayground_Android_UPL.xml | 49 --------------- 4 files changed, 5 insertions(+), 119 deletions(-) delete mode 100644 sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 013970b3f..18c1395aa 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -244,7 +244,8 @@ BeforeAll { # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $global:AndroidCrashResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments "-e test crash-capture" + # $cmdlineCrashArgs = "-e cmdline '-crash-capture'" + # $global:AndroidCrashResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan @@ -256,7 +257,8 @@ BeforeAll { Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow # TODO: When AndroidProvider is added to app-runner: Invoke-DeviceApp $script:ActivityName -Arguments "-e test message-capture" - $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments "-e test message-capture" + $cmdlineMessageArgs = "-e cmdline '-message-capture'" + $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan } diff --git a/sample/Source/SentryPlayground/SentryPlayground.Build.cs b/sample/Source/SentryPlayground/SentryPlayground.Build.cs index ae2b24458..5abaab464 100644 --- a/sample/Source/SentryPlayground/SentryPlayground.Build.cs +++ b/sample/Source/SentryPlayground/SentryPlayground.Build.cs @@ -20,12 +20,5 @@ public SentryPlayground(ReadOnlyTargetRules Target) : base(Target) // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true - - // Register Android UPL for Intent extras handling (integration tests) - if (Target.Platform == UnrealTargetPlatform.Android) - { - string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath); - AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "SentryPlayground_Android_UPL.xml")); - } } } diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index 64aa7d9dc..e5421c64e 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -14,73 +14,13 @@ #include "Misc/CommandLine.h" #include "Engine/Engine.h" -#if PLATFORM_ANDROID -#include "Android/AndroidJNI.h" -#include "Android/AndroidApplication.h" - -// Helper function to get command-line arguments from Android Intent extras -// Calls the AndroidThunkJava_GetIntentCommandLine() method added via UPL -FString GetIntentCommandLineFromAndroid() -{ - FString Result; - - if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) - { - // Get GameActivity class - jclass GameActivityClass = FAndroidApplication::FindJavaClass("com/epicgames/unreal/GameActivity"); - if (GameActivityClass != nullptr) - { - // Find our custom method: AndroidThunkJava_GetIntentCommandLine() - jmethodID GetIntentCmdLineMethod = Env->GetMethodID( - GameActivityClass, - "AndroidThunkJava_GetIntentCommandLine", - "()Ljava/lang/String;" - ); - - if (GetIntentCmdLineMethod != nullptr) - { - // Get the GameActivity instance - jobject GameActivityObj = FAndroidApplication::GetGameActivityThis(); - if (GameActivityObj != nullptr) - { - // Call the method - jstring JavaResult = (jstring)Env->CallObjectMethod(GameActivityObj, GetIntentCmdLineMethod); - if (JavaResult != nullptr) - { - // Convert Java string to FString - const char* JavaChars = Env->GetStringUTFChars(JavaResult, nullptr); - Result = FString(UTF8_TO_TCHAR(JavaChars)); - Env->ReleaseStringUTFChars(JavaResult, JavaChars); - Env->DeleteLocalRef(JavaResult); - } - } - } - - Env->DeleteLocalRef(GameActivityClass); - } - } - - return Result; -} -#endif - void USentryPlaygroundGameInstance::Init() { Super::Init(); FString CommandLine = FCommandLine::Get(); -#if PLATFORM_ANDROID - // On Android, merge Intent extras into command line - // Intent extras are passed via: adb shell am start -n / -e test crash-capture - FString IntentCommandLine = GetIntentCommandLineFromAndroid(); - if (!IntentCommandLine.IsEmpty()) - { - UE_LOG(LogSentrySample, Display, TEXT("[SentryPlayground] Intent command line: %s"), *IntentCommandLine); - // Prepend Intent args so they take precedence over default command line - CommandLine = IntentCommandLine + TEXT(" ") + CommandLine; - } -#endif + UE_LOG(LogSentrySample, Display, TEXT("Startin app with commandline: %s\n"), *CommandLine); // Check for expected test parameters to decide between running integration tests // or launching the sample app with UI for manual testing diff --git a/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml b/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml deleted file mode 100644 index 0e0de826d..000000000 --- a/sample/Source/SentryPlayground/SentryPlayground_Android_UPL.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - -// Public method to extract Intent extras and return as command-line string -// Called from C++ via JNI in SentryPlaygroundGameInstance -public String AndroidThunkJava_GetIntentCommandLine() { - android.content.Intent intent = getIntent(); - if (intent == null) { - return ""; - } - - StringBuilder cmdLine = new StringBuilder(); - - // Extract test type from Intent extras - // Usage: adb shell am start -n <package>/<activity> -e test crash-capture - String testType = intent.getStringExtra("test"); - if (testType != null) { - if (testType.equals("crash-capture")) { - cmdLine.append("-crash-capture "); - } else if (testType.equals("message-capture")) { - cmdLine.append("-message-capture "); - } - } - - // Extract optional DSN override - // Usage: adb shell am start -n <package>/<activity> -e dsn "https://..." - String dsn = intent.getStringExtra("dsn"); - if (dsn != null) { - cmdLine.append("-dsn=\"").append(dsn).append("\" "); - } - - // Add standard test flags for headless execution - cmdLine.append("-nullrhi -unattended -stdout -nosplash"); - - String result = cmdLine.toString().trim(); - if (!result.isEmpty()) { - android.util.Log.d("SentryPlayground", "Intent command line: " + result); - } - - return result; -} - - - \ No newline at end of file From 3bc3f33c21dbabd2948013609ea4206412768753 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 13 Nov 2025 16:39:01 +0200 Subject: [PATCH 13/52] Clean up comments --- integration-test/Integration.Tests.Android.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.ps1 index 18c1395aa..08e83dd57 100644 --- a/integration-test/Integration.Tests.Android.ps1 +++ b/integration-test/Integration.Tests.Android.ps1 @@ -256,7 +256,6 @@ BeforeAll { # TODO: use -SkipReinstall to preserve the crash state. Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - # TODO: When AndroidProvider is added to app-runner: Invoke-DeviceApp $script:ActivityName -Arguments "-e test message-capture" $cmdlineMessageArgs = "-e cmdline '-message-capture'" $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs From c9079bce9f549b6d32fae3852c7d77ec45419d73 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 20 Nov 2025 15:33:49 +0200 Subject: [PATCH 14/52] Test SauceLabs Fix artifact download Shell Fix Fix Fix Fix Set config Fix syntax Sauce skip run Update appium Test test test apk name Test Test Test Test Test Test Test Test Test Test Test tets Test Test Poll session status Test Test Test 2 runs Test Test Try fix redirect Test Test fix id Test app status poll Test Clean up --- .github/workflows/ci.yml | 3 +- .../workflows/integration-test-android.yml | 244 ++++++++++++++---- 2 files changed, 200 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf1cd41e8..5904ba332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,14 +200,13 @@ jobs: unreal-version: ${{ matrix.unreal }} integration-test-android: - needs: [test-android] name: Android UE ${{ matrix.unreal }} secrets: inherit strategy: fail-fast: false matrix: # Starting with UE 5.4-5.6 for faster iteration - unreal: ['5.4', '5.5', '5.6'] + unreal: ['5.4'] uses: ./.github/workflows/integration-test-android.yml with: unreal-version: ${{ matrix.unreal }} diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 4a38a0146..6fc804fd9 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -10,15 +10,20 @@ jobs: name: Integration Test runs-on: ubuntu-latest + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + GITHUB_TOKEN: ${{ github.token }} + SAUCE_REGION: eu-central-1 + steps: - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Download sample build - uses: actions/download-artifact@v4 + - name: Download artifact from workflow run + uses: dawidd6/action-download-artifact@v6 with: - name: UE ${{ inputs.unreal-version }} sample build (Android) + run_id: 19533023161 + name: "UE ${{ inputs.unreal-version }} sample build (Android)" path: sample-build - name: List downloaded files @@ -27,17 +32,6 @@ jobs: ls -lah sample-build/ echo "Using x64 APK for emulator testing" - - name: Enable KVM group perms - 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: Setup Android directories - run: | - mkdir -p $HOME/.android/avd - touch $HOME/.android/repositories.cfg - - name: Install Pester shell: pwsh run: Install-Module -Name Pester -Force -SkipPublisherCheck @@ -49,37 +43,197 @@ jobs: mkdir build cmake -B build -S . - - name: Run Android Integration Tests - uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b - id: integration-test - timeout-minutes: 45 - with: - api-level: 34 - target: 'google_apis' - arch: x86_64 - force-avd-creation: true - disable-animations: true - disable-spellchecker: true - emulator-options: > - -no-window - -no-snapshot-save - -gpu swiftshader_indirect - -noaudio - -no-boot-anim - -camera-back none - -camera-front none - script: | - adb wait-for-device - cd integration-test && pwsh -Command "Invoke-Pester Integration.Tests.Android.ps1 -CI" - env: - SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} - SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-x64.apk + - name: Upload APK to SauceLabs Storage + id: upload_apk + run: | + sudo apt-get install -y jq + + APK=$(ls sample-build/*.apk | head -n1) + echo "Uploading: $APK" + + RESPONSE=$(curl -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://api.${SAUCE_REGION}.saucelabs.com/v1/storage/upload" \ + -F "payload=@${APK}" \ + -F "name=$(basename "$APK")") + + echo "Upload Response: $RESPONSE" + STORAGE_ID=$(echo "$RESPONSE" | jq -r '.item.id') + + echo "storage_id=$STORAGE_ID" >> $GITHUB_OUTPUT + + - name: Start UE5 test on real device + id: start_session + run: | + REQUEST=$(cat <> $GITHUB_OUTPUT + + - name: First launch (crash capture) + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + + RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \ + -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \ + -H "Content-Type: application/json" \ + -d '{ + "appPackage": "io.sentry.unreal.sample", + "appActivity": "com.epicgames.unreal.GameActivity", + "appWaitActivity": "*", + "intentAction": "android.intent.action.MAIN", + "intentCategory": "android.intent.category.LAUNCHER", + "optionalIntentArguments": "-e cmdline '-crash-capture'" + }') + echo "Launch response: $RESPONSE" + + - name: Wait for app to finish (first run) + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + + echo "Waiting for app to finish..." + MAX_WAIT=20 # Max wait time in seconds + POLL_INTERVAL=2 + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT ]; do + STATE=$(curl -sSL \ + -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \ + -H "Content-Type: application/json" \ + -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \ + | jq -r '.value') + + echo "App state: $STATE (elapsed: ${ELAPSED}s)" + + # State 1 = not running, 0 = not installed + if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then + echo "App finished/crashed" + break + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT ]; then + echo "Timeout waiting for app to finish" + fi + + - name: Get first run logs + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \ + -H "Content-Type: application/json" \ + -d '{"type": "logcat"}' \ + -o first_run_logs.json + + - name: Second launch (message capture) + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + + RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \ + -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \ + -H "Content-Type: application/json" \ + -d '{ + "appPackage": "io.sentry.unreal.sample", + "appActivity": "com.epicgames.unreal.GameActivity", + "appWaitActivity": "*", + "intentAction": "android.intent.action.MAIN", + "intentCategory": "android.intent.category.LAUNCHER", + "optionalIntentArguments": "-e cmdline '-message-capture'" + }') + echo "Launch response: $RESPONSE" + + - name: Wait for app to finish (second run) + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + + echo "Waiting for app to finish..." + MAX_WAIT=20 # Max wait time in seconds + POLL_INTERVAL=2 + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT ]; do + STATE=$(curl -sSL \ + -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \ + -H "Content-Type: application/json" \ + -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \ + | jq -r '.value') + + echo "App state: $STATE (elapsed: ${ELAPSED}s)" + + # State 1 = not running, 0 = not installed + if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then + echo "App finished/crashed" + break + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT ]; then + echo "Timeout waiting for app to finish" + fi + + - name: Get second run logs + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \ + -H "Content-Type: application/json" \ + -d '{"type": "logcat"}' \ + -o second_run_logs.json + + - name: End session + if: always() + run: | + SESSION_ID="${{ steps.start_session.outputs.session_id }}" + if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "null" ]; then + curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ + -X DELETE "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID" + fi - name: Upload integration test output - if: ${{ always() && steps.integration-test.outcome == 'failure' }} + if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: UE ${{ inputs.unreal-version }} integration test output (Android) - path: integration-test/output/ + name: UE ${{ inputs.unreal-version }} device logs (Android) + path: | + first_run_logs.json + second_run_logs.json retention-days: 14 \ No newline at end of file From 957c826b1429686d2095ffcd2226ce59849f3e65 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 11:58:11 +0200 Subject: [PATCH 15/52] Add separate SauceLabs test script --- .../workflows/integration-test-android.yml | 202 +----- .../Integration.Tests.Android.SauceLabs.ps1 | 585 ++++++++++++++++++ 2 files changed, 603 insertions(+), 184 deletions(-) create mode 100644 integration-test/Integration.Tests.Android.SauceLabs.ps1 diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 6fc804fd9..0357dfcd6 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -30,7 +30,7 @@ jobs: run: | echo "Downloaded build contents:" ls -lah sample-build/ - echo "Using x64 APK for emulator testing" + echo "Using APK for SauceLabs real device testing" - name: Install Pester shell: pwsh @@ -43,197 +43,31 @@ jobs: mkdir build cmake -B build -S . - - name: Upload APK to SauceLabs Storage - id: upload_apk - run: | - sudo apt-get install -y jq - - APK=$(ls sample-build/*.apk | head -n1) - echo "Uploading: $APK" - - RESPONSE=$(curl -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://api.${SAUCE_REGION}.saucelabs.com/v1/storage/upload" \ - -F "payload=@${APK}" \ - -F "name=$(basename "$APK")") - - echo "Upload Response: $RESPONSE" - STORAGE_ID=$(echo "$RESPONSE" | jq -r '.item.id') - - echo "storage_id=$STORAGE_ID" >> $GITHUB_OUTPUT - - - name: Start UE5 test on real device - id: start_session + - name: Run integration tests + id: run-integration-tests + shell: pwsh + env: + SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} + UNREAL_VERSION: ${{ inputs.unreal-version }} run: | - REQUEST=$(cat <> $GITHUB_OUTPUT - - - name: First launch (crash capture) - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - - RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \ - -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \ - -H "Content-Type: application/json" \ - -d '{ - "appPackage": "io.sentry.unreal.sample", - "appActivity": "com.epicgames.unreal.GameActivity", - "appWaitActivity": "*", - "intentAction": "android.intent.action.MAIN", - "intentCategory": "android.intent.category.LAUNCHER", - "optionalIntentArguments": "-e cmdline '-crash-capture'" - }') - echo "Launch response: $RESPONSE" - - - name: Wait for app to finish (first run) - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - - echo "Waiting for app to finish..." - MAX_WAIT=20 # Max wait time in seconds - POLL_INTERVAL=2 - ELAPSED=0 - - while [ $ELAPSED -lt $MAX_WAIT ]; do - STATE=$(curl -sSL \ - -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \ - -H "Content-Type: application/json" \ - -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \ - | jq -r '.value') - - echo "App state: $STATE (elapsed: ${ELAPSED}s)" - - # State 1 = not running, 0 = not installed - if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then - echo "App finished/crashed" - break - fi - - sleep $POLL_INTERVAL - ELAPSED=$((ELAPSED + POLL_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT ]; then - echo "Timeout waiting for app to finish" - fi - - - name: Get first run logs - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \ - -H "Content-Type: application/json" \ - -d '{"type": "logcat"}' \ - -o first_run_logs.json - - - name: Second launch (message capture) - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - - RESPONSE=$(curl -sSL -w "\nHTTP:%{http_code}" \ - -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/appium/device/start_activity" \ - -H "Content-Type: application/json" \ - -d '{ - "appPackage": "io.sentry.unreal.sample", - "appActivity": "com.epicgames.unreal.GameActivity", - "appWaitActivity": "*", - "intentAction": "android.intent.action.MAIN", - "intentCategory": "android.intent.category.LAUNCHER", - "optionalIntentArguments": "-e cmdline '-message-capture'" - }') - echo "Launch response: $RESPONSE" - - - name: Wait for app to finish (second run) - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - - echo "Waiting for app to finish..." - MAX_WAIT=20 # Max wait time in seconds - POLL_INTERVAL=2 - ELAPSED=0 - - while [ $ELAPSED -lt $MAX_WAIT ]; do - STATE=$(curl -sSL \ - -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/execute/sync" \ - -H "Content-Type: application/json" \ - -d '{"script": "mobile: queryAppState", "args": [{"appId": "io.sentry.unreal.sample"}]}' \ - | jq -r '.value') - - echo "App state: $STATE (elapsed: ${ELAPSED}s)" - - # State 1 = not running, 0 = not installed - if [ "$STATE" = "1" ] || [ "$STATE" = "0" ]; then - echo "App finished/crashed" - break - fi - - sleep $POLL_INTERVAL - ELAPSED=$((ELAPSED + POLL_INTERVAL)) - done - - if [ $ELAPSED -ge $MAX_WAIT ]; then - echo "Timeout waiting for app to finish" - fi - - name: Get second run logs - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X POST "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID/log" \ - -H "Content-Type: application/json" \ - -d '{"type": "logcat"}' \ - -o second_run_logs.json + Write-Host "Found APK: $($apkPath.Name)" + $env:SENTRY_UNREAL_TEST_APP_PATH = $apkPath.FullName - - name: End session - if: always() - run: | - SESSION_ID="${{ steps.start_session.outputs.session_id }}" - if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "null" ]; then - curl -sS -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \ - -X DELETE "https://ondemand.eu-central-1.saucelabs.com/wd/hub/session/$SESSION_ID" - fi + cd integration-test + Invoke-Pester Integration.Tests.Android.SauceLabs.ps1 -CI - name: Upload integration test output if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: UE ${{ inputs.unreal-version }} device logs (Android) + name: UE ${{ inputs.unreal-version }} integration test output (Android) path: | - first_run_logs.json - second_run_logs.json + integration-test/output/ retention-days: 14 \ No newline at end of file diff --git a/integration-test/Integration.Tests.Android.SauceLabs.ps1 b/integration-test/Integration.Tests.Android.SauceLabs.ps1 new file mode 100644 index 000000000..bee1bc961 --- /dev/null +++ b/integration-test/Integration.Tests.Android.SauceLabs.ps1 @@ -0,0 +1,585 @@ +# Integration tests for Sentry Unreal SDK on Android via SauceLabs Real Device Cloud +# Requires: +# - Pre-built APK +# - SauceLabs account credentials +# - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH +# SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Script-level state for session management +$script:SessionId = $null +$script:LogLineCount = 0 # Track log lines read so far (for delta) + +function script:Invoke-SauceLabsApi { + param( + [Parameter(Mandatory)] + [string]$Method, + + [Parameter(Mandatory)] + [string]$Uri, + + [Parameter()] + [hashtable]$Body = $null, + + [Parameter()] + [string]$ContentType = 'application/json', + + [Parameter()] + [switch]$IsMultipart, + + [Parameter()] + [string]$FilePath + ) + + $username = $env:SAUCE_USERNAME + $accessKey = $env:SAUCE_ACCESS_KEY + + if (-not $username -or -not $accessKey) { + throw "SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables must be set" + } + + $base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${username}:${accessKey}")) + $headers = @{ + 'Authorization' = "Basic $base64Auth" + } + + try { + if ($IsMultipart) { + # Use curl for multipart uploads (PowerShell's Invoke-WebRequest struggles with this) + $curlCmd = "curl -u `"$username`:$accessKey`" -X $Method `"$Uri`" -F `"payload=@$FilePath`" -F `"name=$(Split-Path $FilePath -Leaf)`"" + Write-Debug "Executing: $curlCmd" + $response = Invoke-Expression $curlCmd | ConvertFrom-Json + return $response + } else { + $params = @{ + Uri = $Uri + Method = $Method + Headers = $headers + } + + if ($Body) { + $params['Body'] = ($Body | ConvertTo-Json -Depth 10) + $params['ContentType'] = $ContentType + } + + $response = Invoke-RestMethod @params + return $response + } + } catch { + Write-Error "SauceLabs API call failed: $($_.Exception.Message)" + if ($_.Exception.Response) { + $reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) + $responseBody = $reader.ReadToEnd() + Write-Error "Response: $responseBody" + } + throw + } +} + +function script:Install-SauceLabsApp { + param( + [Parameter(Mandatory)] + [string]$ApkPath, + + [Parameter(Mandatory)] + [string]$Region + ) + + if (-not (Test-Path $ApkPath)) { + throw "APK file not found: $ApkPath" + } + + if ($ApkPath -notlike '*.apk') { + throw "Package must be an .apk file. Got: $ApkPath" + } + + Write-Host "Uploading APK to SauceLabs Storage..." -ForegroundColor Yellow + $uploadUri = "https://api.${Region}.saucelabs.com/v1/storage/upload" + + $response = Invoke-SauceLabsApi -Method POST -Uri $uploadUri -IsMultipart -FilePath $ApkPath + + if (-not $response.item.id) { + throw "Failed to upload APK: No storage ID in response" + } + + $storageId = $response.item.id + Write-Host "APK uploaded successfully. Storage ID: $storageId" -ForegroundColor Green + + return $storageId +} + +function script:Initialize-SauceLabsSession { + param( + [Parameter(Mandatory)] + [string]$StorageId, + + [Parameter(Mandatory)] + [string]$DeviceName, + + [Parameter(Mandatory)] + [string]$Region, + + [Parameter(Mandatory)] + [string]$UnrealVersion + ) + + Write-Host "Creating SauceLabs Appium session..." -ForegroundColor Yellow + + $sessionUri = "https://ondemand.${Region}.saucelabs.com/wd/hub/session" + + $capabilities = @{ + capabilities = @{ + alwaysMatch = @{ + platformName = "Android" + 'appium:app' = "storage:$StorageId" + 'appium:deviceName' = $DeviceName + 'appium:automationName' = "UiAutomator2" + 'appium:noReset' = $true + 'appium:autoLaunch' = $false + 'sauce:options' = @{ + name = "$UnrealVersion Android Integration Test" + appiumVersion = "latest" + } + } + } + } + + $response = Invoke-SauceLabsApi -Method POST -Uri $sessionUri -Body $capabilities + + $sessionId = $response.value.sessionId + if (-not $sessionId) { + $sessionId = $response.sessionId + } + + if (-not $sessionId) { + throw "Failed to create session: No session ID in response" + } + + Write-Host "Session created successfully. Session ID: $sessionId" -ForegroundColor Green + + return $sessionId +} + +function script:Invoke-SauceLabsApp { + param( + [Parameter(Mandatory)] + [string]$PackageName, + + [Parameter(Mandatory)] + [string]$ActivityName, + + [Parameter()] + [string]$Arguments = "", + + [Parameter(Mandatory)] + [string]$Region + ) + + $sessionId = $script:SessionId + if (-not $sessionId) { + throw "No active SauceLabs session. Call Initialize-SauceLabsSession first." + } + + $baseUri = "https://ondemand.${Region}.saucelabs.com/wd/hub/session/$sessionId" + $outputDir = $script:OutputDir + + # Configuration + $timeoutSeconds = 300 + $pollIntervalSeconds = 2 + + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $logFile = if ($OutputDir) { "$OutputDir/$timestamp-logcat.txt" } else { $null } + + # Launch activity with Intent extras + Write-Host "Launching: $PackageName/$ActivityName" -ForegroundColor Cyan + if ($Arguments) { + Write-Host " Arguments: $Arguments" -ForegroundColor Cyan + } + + $launchBody = @{ + appPackage = $PackageName + appActivity = $ActivityName + appWaitActivity = "*" + intentAction = "android.intent.action.MAIN" + intentCategory = "android.intent.category.LAUNCHER" + } + + if ($Arguments) { + $launchBody['optionalIntentArguments'] = $Arguments + } + + try { + $launchResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/appium/device/start_activity" -Body $launchBody + Write-Debug "Launch response: $($launchResponse | ConvertTo-Json)" + } catch { + throw "Failed to launch activity: $_" + } + + # Wait a moment for app to start + Start-Sleep -Seconds 3 + + # Poll app state until it exits or completes + Write-Host "Monitoring app execution..." -ForegroundColor Yellow + $startTime = Get-Date + $completed = $false + + while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($timeoutSeconds)) { + # Query app state + $stateBody = @{ + script = "mobile: queryAppState" + args = @( + @{ appId = $PackageName } + ) + } + + try { + $stateResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/execute/sync" -Body $stateBody + $appState = $stateResponse.value + + Write-Debug "App state: $appState (elapsed: $([int]((Get-Date) - $startTime).TotalSeconds)s)" + + # State 1 = not running, 0 = not installed + if ($appState -eq 1 -or $appState -eq 0) { + Write-Host "App finished/crashed (state: $appState)" -ForegroundColor Green + $completed = $true + break + } + + # Also check logs for completion markers + $logBody = @{ type = "logcat" } + $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody + + if ($logResponse.value -and $logResponse.value.Count -gt $script:LogLineCount) { + $newLogs = $logResponse.value | Select-Object -Skip $script:LogLineCount + $logMessages = $newLogs | ForEach-Object { $_.message } + + # Check for completion markers + if (($logMessages | Where-Object { $_ -match "TEST_RESULT:" }) -or + ($logMessages | Where-Object { $_ -match "Requesting app exit" })) { + Write-Host "Test completion detected in logs" -ForegroundColor Green + $completed = $true + break + } + } + } catch { + Write-Warning "Failed to query app state: $_" + } + + Start-Sleep -Seconds $pollIntervalSeconds + } + + if (-not $completed) { + Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow + } + + # Retrieve final logs (delta from last read) + Write-Host "Retrieving logs..." -ForegroundColor Yellow + $logBody = @{ type = "logcat" } + $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody + + # Extract new log lines (delta) + [array]$allLogs = @() + if ($logResponse.value) { + $allLogs = $logResponse.value | Select-Object -Skip $script:LogLineCount + $script:LogLineCount = $logResponse.value.Count + } + + # Convert SauceLabs log format to text (matching adb output) + $logCache = $allLogs | ForEach-Object { + $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } + $level = if ($_.level) { $_.level } else { "" } + $message = if ($_.message) { $_.message } else { "" } + "$timestamp $level $message" + } + + # Save logs to file if OutputDir specified + if ($logFile) { + $logCache | Out-File $logFile + Write-Host "Logs saved to: $logFile" -ForegroundColor Cyan + } + + # Return structured result (matches app-runner pattern) + return @{ + ExitCode = 0 # Android apps don't report exit codes + Output = $logCache + Error = @() + } +} + +BeforeAll { + # Check if configuration file exists + $configFile = "$PSScriptRoot/TestConfig.local.ps1" + if (-not (Test-Path $configFile)) { + throw "Configuration file '$configFile' not found. Run 'cmake -B build -S .' first" + } + + # Load configuration (provides $global:AppRunnerPath) + . $configFile + + # Import app-runner modules (SentryApiClient, test utilities) + . "$global:AppRunnerPath/import-modules.ps1" + + # Validate environment variables + $script:DSN = $env:SENTRY_UNREAL_TEST_DSN + $script:AuthToken = $env:SENTRY_AUTH_TOKEN + $script:ApkPath = $env:SENTRY_UNREAL_TEST_APP_PATH + $script:SauceUsername = $env:SAUCE_USERNAME + $script:SauceAccessKey = $env:SAUCE_ACCESS_KEY + $script:SauceRegion = $env:SAUCE_REGION + + if (-not $script:DSN) { + throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" + } + + if (-not $script:AuthToken) { + throw "Environment variable SENTRY_AUTH_TOKEN must be set" + } + + if (-not $script:ApkPath) { + throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" + } + + if (-not $script:SauceUsername) { + throw "Environment variable SAUCE_USERNAME must be set" + } + + if (-not $script:SauceAccessKey) { + throw "Environment variable SAUCE_ACCESS_KEY must be set" + } + + if (-not $script:SauceRegion) { + throw "Environment variable SAUCE_REGION must be set" + } + + # Connect to Sentry API + Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow + Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken + + # Validate app path + if (-not (Test-Path $script:ApkPath)) { + throw "Application not found at: $script:ApkPath" + } + + # Create output directory + $script:OutputDir = "$PSScriptRoot/output" + if (-not (Test-Path $script:OutputDir)) { + New-Item -ItemType Directory -Path $script:OutputDir | Out-Null + } + + $script:PackageName = "io.sentry.unreal.sample" + $script:ActivityName = "com.epicgames.unreal.GameActivity" + $script:DeviceName = "Samsung_Galaxy_S23_FE_free" + $script:UnrealVersion = if ($env:UNREAL_VERSION) { $env:UNREAL_VERSION } else { "5.x" } + + # Upload APK to SauceLabs Storage + Write-Host "Uploading APK to SauceLabs..." -ForegroundColor Yellow + $script:StorageId = Install-SauceLabsApp -ApkPath $script:ApkPath -Region $script:SauceRegion + + # Create SauceLabs session (reused for all app launches) + Write-Host "Creating SauceLabs session..." -ForegroundColor Yellow + $script:SessionId = Initialize-SauceLabsSession ` + -StorageId $script:StorageId ` + -DeviceName $script:DeviceName ` + -Region $script:SauceRegion ` + -UnrealVersion $script:UnrealVersion + + # ========================================== + # RUN 1: Crash test - creates minidump + # ========================================== + # The crash is captured but NOT uploaded yet (Android behavior). + # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) + + # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow + # $cmdlineCrashArgs = "-e cmdline '-crash-capture'" + # $global:SauceLabsCrashResult = Invoke-SauceLabsApp ` + # -PackageName $script:PackageName ` + # -ActivityName $script:ActivityName ` + # -Arguments $cmdlineCrashArgs ` + # -Region $script:SauceRegion + + # Write-Host "Crash test exit code: $($global:SauceLabsCrashResult.ExitCode)" -ForegroundColor Cyan + + # ========================================== + # RUN 2: Message test - uploads crash from Run 1 + captures message + # ========================================== + # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. + + Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow + $cmdlineMessageArgs = "-e cmdline '-message-capture'" + $global:SauceLabsMessageResult = Invoke-SauceLabsApp ` + -PackageName $script:PackageName ` + -ActivityName $script:ActivityName ` + -Arguments $cmdlineMessageArgs ` + -Region $script:SauceRegion + + Write-Host "Message test exit code: $($global:SauceLabsMessageResult.ExitCode)" -ForegroundColor Cyan +} + +Describe "Sentry Unreal Android Integration Tests (SauceLabs)" { + + # ========================================== + # NOTE: Crash Capture Tests are DISABLED due to tag sync issue + # Uncomment when Android SDK tag persistence is fixed + # ========================================== + # Context "Crash Capture Tests" { + # BeforeAll { + # # Crash event is sent during the MESSAGE run (Run 2) + # # But the crash_id comes from the CRASH run (Run 1) + # $CrashResult = $global:SauceLabsCrashResult + # $CrashEvent = $null + # + # # Parse crash event ID from crash run output + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + # + # if ($eventIds -and $eventIds.Count -gt 0) { + # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan + # $crashId = $eventIds[0] + # + # # Fetch crash event using the tag (event was sent during message run) + # try { + # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green + # } catch { + # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red + # } + # } else { + # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow + # } + # } + # + # It "Should output event ID before crash" { + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + # $eventIds | Should -Not -BeNullOrEmpty + # $eventIds.Count | Should -Be 1 + # } + # + # It "Should capture crash event in Sentry (uploaded during next run)" { + # $CrashEvent | Should -Not -BeNullOrEmpty + # } + # + # It "Should have correct event type and platform" { + # $CrashEvent.type | Should -Be 'error' + # $CrashEvent.platform | Should -Be 'native' + # } + # + # It "Should have exception information" { + # $CrashEvent.exception | Should -Not -BeNullOrEmpty + # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty + # } + # + # It "Should have stack trace" { + # $exception = $CrashEvent.exception.values[0] + # $exception.stacktrace | Should -Not -BeNullOrEmpty + # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty + # } + # + # It "Should have user context" { + # $CrashEvent.user | Should -Not -BeNullOrEmpty + # $CrashEvent.user.username | Should -Be 'TestUser' + # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' + # $CrashEvent.user.id | Should -Be '12345' + # } + # + # It "Should have test.crash_id tag for correlation" { + # $tags = $CrashEvent.tags + # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } + # $crashIdTag | Should -Not -BeNullOrEmpty + # $crashIdTag.value | Should -Not -BeNullOrEmpty + # } + # + # It "Should have integration test tag" { + # $tags = $CrashEvent.tags + # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + # } + # + # It "Should have breadcrumbs from before crash" { + # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + # } + # } + + Context "Message Capture Tests" { + BeforeAll { + $MessageResult = $global:SauceLabsMessageResult + $MessageEvent = $null + + # Parse event ID from output + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 + + if ($eventIds -and $eventIds.Count -gt 0) { + Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan + + # Fetch event from Sentry (with polling) + try { + $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] + Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green + } catch { + Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red + } + } else { + Write-Host "Warning: No message event ID found in output" -ForegroundColor Yellow + } + } + + It "Should output event ID" { + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 + $eventIds | Should -Not -BeNullOrEmpty + $eventIds.Count | Should -Be 1 + } + + It "Should output TEST_RESULT with success" { + $testResultLine = $MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine | Should -Not -BeNullOrEmpty + $testResultLine | Should -Match '"success"\s*:\s*true' + } + + It "Should capture message event in Sentry" { + $MessageEvent | Should -Not -BeNullOrEmpty + } + + It "Should have correct platform" { + # Android events are captured from Java layer, so platform is 'java' not 'native' + $MessageEvent.platform | Should -Be 'java' + } + + It "Should have message content" { + $MessageEvent.message | Should -Not -BeNullOrEmpty + $MessageEvent.message.formatted | Should -Match 'Integration test message' + } + + It "Should have user context" { + $MessageEvent.user | Should -Not -BeNullOrEmpty + $MessageEvent.user.username | Should -Be 'TestUser' + } + + It "Should have integration test tag" { + $tags = $MessageEvent.tags + ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + } + + It "Should have breadcrumbs" { + $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } +} + +AfterAll { + # Clean up SauceLabs session + if ($script:SessionId) { + Write-Host "Ending SauceLabs session..." -ForegroundColor Yellow + try { + $sessionUri = "https://ondemand.$($script:SauceRegion).saucelabs.com/wd/hub/session/$($script:SessionId)" + Invoke-SauceLabsApi -Method DELETE -Uri $sessionUri + Write-Host "Session ended successfully" -ForegroundColor Green + } catch { + Write-Warning "Failed to end session: $_" + } + } + + Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow + Disconnect-SentryApi + Write-Host "Integration tests complete" -ForegroundColor Green +} From 11d997aee613714d3449400ce2ae2b97ed5bc4da Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 12:05:26 +0200 Subject: [PATCH 16/52] Fix log retrieval --- .../Integration.Tests.Android.SauceLabs.ps1 | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/integration-test/Integration.Tests.Android.SauceLabs.ps1 b/integration-test/Integration.Tests.Android.SauceLabs.ps1 index bee1bc961..cd6b4d10e 100644 --- a/integration-test/Integration.Tests.Android.SauceLabs.ps1 +++ b/integration-test/Integration.Tests.Android.SauceLabs.ps1 @@ -252,8 +252,8 @@ function script:Invoke-SauceLabsApp { $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody if ($logResponse.value -and $logResponse.value.Count -gt $script:LogLineCount) { - $newLogs = $logResponse.value | Select-Object -Skip $script:LogLineCount - $logMessages = $newLogs | ForEach-Object { $_.message } + $newLogs = @($logResponse.value | Select-Object -Skip $script:LogLineCount) + $logMessages = @($newLogs | ForEach-Object { $_.message }) # Check for completion markers if (($logMessages | Where-Object { $_ -match "TEST_RESULT:" }) -or @@ -281,17 +281,33 @@ function script:Invoke-SauceLabsApp { # Extract new log lines (delta) [array]$allLogs = @() - if ($logResponse.value) { - $allLogs = $logResponse.value | Select-Object -Skip $script:LogLineCount - $script:LogLineCount = $logResponse.value.Count + if ($logResponse.value -and $logResponse.value.Count -gt 0) { + $totalLogCount = $logResponse.value.Count + Write-Debug "Total logs in response: $totalLogCount, Previously read: $script:LogLineCount" + + if ($totalLogCount -gt $script:LogLineCount) { + # Get only new logs (skip previously read lines) + $allLogs = @($logResponse.value | Select-Object -Skip $script:LogLineCount) + Write-Host "Retrieved $($allLogs.Count) new log lines" -ForegroundColor Cyan + } else { + Write-Host "No new log lines since last read" -ForegroundColor Yellow + } + + # Update counter for next read + $script:LogLineCount = $totalLogCount + } else { + Write-Host "No logs available in response" -ForegroundColor Yellow } # Convert SauceLabs log format to text (matching adb output) - $logCache = $allLogs | ForEach-Object { - $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } - $level = if ($_.level) { $_.level } else { "" } - $message = if ($_.message) { $_.message } else { "" } - "$timestamp $level $message" + $logCache = @() + if ($allLogs -and $allLogs.Count -gt 0) { + $logCache = $allLogs | ForEach-Object { + $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } + $level = if ($_.level) { $_.level } else { "" } + $message = if ($_.message) { $_.message } else { "" } + "$timestamp $level $message" + } } # Save logs to file if OutputDir specified From f87da414c331205acf4dd3e09b2a7aa0f16844a4 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 12:21:24 +0200 Subject: [PATCH 17/52] Fix --- .../Integration.Tests.Android.SauceLabs.ps1 | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/integration-test/Integration.Tests.Android.SauceLabs.ps1 b/integration-test/Integration.Tests.Android.SauceLabs.ps1 index cd6b4d10e..6ec5e115b 100644 --- a/integration-test/Integration.Tests.Android.SauceLabs.ps1 +++ b/integration-test/Integration.Tests.Android.SauceLabs.ps1 @@ -252,8 +252,9 @@ function script:Invoke-SauceLabsApp { $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody if ($logResponse.value -and $logResponse.value.Count -gt $script:LogLineCount) { - $newLogs = @($logResponse.value | Select-Object -Skip $script:LogLineCount) - $logMessages = @($newLogs | ForEach-Object { $_.message }) + # Use array slicing instead of Select-Object -Skip (more reliable) + $newLogs = @($logResponse.value[$script:LogLineCount..($logResponse.value.Count - 1)]) + $logMessages = @($newLogs | ForEach-Object { if ($_) { $_.message } }) # Check for completion markers if (($logMessages | Where-Object { $_ -match "TEST_RESULT:" }) -or @@ -286,8 +287,8 @@ function script:Invoke-SauceLabsApp { Write-Debug "Total logs in response: $totalLogCount, Previously read: $script:LogLineCount" if ($totalLogCount -gt $script:LogLineCount) { - # Get only new logs (skip previously read lines) - $allLogs = @($logResponse.value | Select-Object -Skip $script:LogLineCount) + # Get only new logs using array slicing (more reliable than Select-Object -Skip) + $allLogs = @($logResponse.value[$script:LogLineCount..($totalLogCount - 1)]) Write-Host "Retrieved $($allLogs.Count) new log lines" -ForegroundColor Cyan } else { Write-Host "No new log lines since last read" -ForegroundColor Yellow @@ -303,11 +304,13 @@ function script:Invoke-SauceLabsApp { $logCache = @() if ($allLogs -and $allLogs.Count -gt 0) { $logCache = $allLogs | ForEach-Object { - $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } - $level = if ($_.level) { $_.level } else { "" } - $message = if ($_.message) { $_.message } else { "" } - "$timestamp $level $message" - } + if ($_) { + $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } + $level = if ($_.level) { $_.level } else { "" } + $message = if ($_.message) { $_.message } else { "" } + "$timestamp $level $message" + } + } | Where-Object { $_ } # Filter out any nulls } # Save logs to file if OutputDir specified From 4ea1b2245eeac37199b5e91869e34d0c66d51611 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 13:23:53 +0200 Subject: [PATCH 18/52] Rename script for running Android integration tests locally --- ...ration.Tests.Android.ps1 => Integration.Tests.Android.Adb.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename integration-test/{Integration.Tests.Android.ps1 => Integration.Tests.Android.Adb.ps1} (100%) diff --git a/integration-test/Integration.Tests.Android.ps1 b/integration-test/Integration.Tests.Android.Adb.ps1 similarity index 100% rename from integration-test/Integration.Tests.Android.ps1 rename to integration-test/Integration.Tests.Android.Adb.ps1 From d09d95e851f9b859807e297172f4a42906802dc9 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 13:31:46 +0200 Subject: [PATCH 19/52] Update readme --- integration-test/README.md | 76 +++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/integration-test/README.md b/integration-test/README.md index 589f88f91..53b855d85 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -2,8 +2,15 @@ This directory contains integration tests for the Sentry Unreal SDK using Pester (PowerShell testing framework). +Supports testing on: +- **Windows** - Desktop (x64) +- **Linux** - Desktop (x64) +- **Android** - Local device/emulator (via adb) or SauceLabs Real Device Cloud + ## Prerequisites +### Common Requirements + - **PowerShell 7+** (Core edition) - **CMake 3.20+** - **Pester 5+** - Install with: `Install-Module -Name Pester -Force -SkipPublisherCheck` @@ -11,7 +18,20 @@ This directory contains integration tests for the Sentry Unreal SDK using Pester - **Environment variables**: - `SENTRY_UNREAL_TEST_DSN` - Sentry test project DSN - `SENTRY_AUTH_TOKEN` - Sentry API authentication token - - `SENTRY_UNREAL_TEST_APP_PATH` - Path to the SentryPlayground executable + - `SENTRY_UNREAL_TEST_APP_PATH` - Path to the SentryPlayground executable/APK + +### Android-Specific Requirements + +#### Option A: Local Testing (via adb) +- **Android device or emulator** connected and visible via `adb devices` +- **ADB (Android Debug Bridge)** installed and in PATH + +#### Option B: Cloud Testing (via SauceLabs) +- **SauceLabs account** with Real Device Cloud access +- **Additional environment variables**: + - `SAUCE_USERNAME` - SauceLabs username + - `SAUCE_ACCESS_KEY` - SauceLabs access key + - `SAUCE_REGION` - SauceLabs region (e.g., `eu-central-1`) ## Setup @@ -39,6 +59,7 @@ This will: 3. Download the appropriate artifact: - `UE X.X sample build (Windows)` for Windows testing - `UE X.X sample build (Linux)` for Linux testing + - `UE X.X sample build (Android)` for Android testing 4. Extract to a known location #### Option B: Build Locally @@ -75,11 +96,43 @@ cd integration-test pwsh -Command "Invoke-Pester Integration.Tests.ps1" ``` +### Android (Local via adb) + +```bash +# Ensure device/emulator is connected +adb devices + +# Set environment variables +export SENTRY_UNREAL_TEST_DSN="https://key@org.ingest.sentry.io/project" +export SENTRY_AUTH_TOKEN="sntrys_your_token_here" +export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" + +# Run tests +cd integration-test +pwsh -Command "Invoke-Pester Integration.Tests.Android.Adb.ps1" +``` + +### Android (Cloud via SauceLabs) + +```bash +# Set environment variables +export SENTRY_UNREAL_TEST_DSN="https://key@org.ingest.sentry.io/project" +export SENTRY_AUTH_TOKEN="sntrys_your_token_here" +export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" +export SAUCE_USERNAME="your-saucelabs-username" +export SAUCE_ACCESS_KEY="your-saucelabs-access-key" +export SAUCE_REGION="eu-central-1" + +# Run tests +cd integration-test +pwsh -Command "Invoke-Pester Integration.Tests.Android.SauceLabs.ps1" +``` + ## Test Coverage The integration tests cover: -### Crash Capture Tests +### Crash Capture Tests _(Windows/Linux)_ - Application crashes with non-zero exit code - Event ID is captured from output (set via `test.crash_id` tag) - Crash event appears in Sentry @@ -89,8 +142,10 @@ The integration tests cover: - Integration test tags are set - Breadcrumbs are collected -### Message Capture Tests -- Application exits cleanly (exit code 0) +**Note**: Crash capture tests are currently disabled on Android due to a known issue with tag persistence across app sessions. + +### Message Capture Tests _(All platforms)_ +- Application exits cleanly (exit code 0 on Windows/Linux, Android doesn't report exit codes) - Event ID is captured from output - TEST_RESULT indicates success - Message event appears in Sentry @@ -99,9 +154,13 @@ The integration tests cover: - Integration test tags are set - Breadcrumbs are collected +**Note**: On Android, events are captured from the Java layer, so the platform will be `java` instead of `native`. + ## Output Test outputs are saved to `integration-test/output/`: + +### Windows/Linux - `*-crash-stdout.log` - Crash test standard output - `*-crash-stderr.log` - Crash test standard error - `*-crash-result.json` - Full crash test result @@ -110,6 +169,13 @@ Test outputs are saved to `integration-test/output/`: - `*-message-result.json` - Full message test result - `event-*.json` - Events fetched from Sentry API +### Android +- `*-logcat.txt` - Logcat output from app execution (one file per launch) +- `event-*.json` - Events fetched from Sentry API + ## CI Integration -See `.github/workflows/integration-test-windows.yml` and `.github/workflows/integration-test-linux.yml` for CI usage examples. +See the following workflow files for CI usage examples: +- `.github/workflows/integration-test-windows.yml` - Windows desktop testing +- `.github/workflows/integration-test-linux.yml` - Linux desktop testing +- `.github/workflows/integration-test-android.yml` - Android testing via SauceLabs Real Device Cloud From 89c6014396f26cb324551c3c900cfd18aed467e9 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 13:40:08 +0200 Subject: [PATCH 20/52] Revert CI tweaks for faster iterations --- .github/workflows/ci.yml | 3 +- .../workflows/integration-test-android.yml | 34 +++++-------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5904ba332..1360e6981 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,8 +205,7 @@ jobs: strategy: fail-fast: false matrix: - # Starting with UE 5.4-5.6 for faster iteration - unreal: ['5.4'] + unreal: ['5.4', '5.5', '5.6', '5.7'] uses: ./.github/workflows/integration-test-android.yml with: unreal-version: ${{ matrix.unreal }} diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 0357dfcd6..97cb86769 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -18,49 +18,31 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - - name: Download artifact from workflow run - uses: dawidd6/action-download-artifact@v6 + - name: Download sample build + uses: actions/download-artifact@v4 with: - run_id: 19533023161 - name: "UE ${{ inputs.unreal-version }} sample build (Android)" + name: UE ${{ inputs.unreal-version }} sample build (Android) path: sample-build - - name: List downloaded files - run: | - echo "Downloaded build contents:" - ls -lah sample-build/ - echo "Using APK for SauceLabs real device testing" - - name: Install Pester shell: pwsh run: Install-Module -Name Pester -Force -SkipPublisherCheck - - name: Setup integration test configuration - shell: pwsh - run: | - cd integration-test - mkdir build - cmake -B build -S . - - name: Run integration tests id: run-integration-tests shell: pwsh env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} + SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-arm64.apk UNREAL_VERSION: ${{ inputs.unreal-version }} run: | - # Find the APK file - $apkPath = Get-ChildItem -Path "${{ github.workspace }}/sample-build" -Filter "*.apk" | Select-Object -First 1 - if (-not $apkPath) { - throw "No APK file found in sample-build directory" - } - - Write-Host "Found APK: $($apkPath.Name)" - $env:SENTRY_UNREAL_TEST_APP_PATH = $apkPath.FullName - cd integration-test + mkdir build + cmake -B build -S . Invoke-Pester Integration.Tests.Android.SauceLabs.ps1 -CI - name: Upload integration test output From b99f9a607e5394d80dd45ef6a19f4c15ed18b05f Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 13:58:49 +0200 Subject: [PATCH 21/52] Clean up --- sample/Source/SentryPlayground/SentryPlayground.Build.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/sample/Source/SentryPlayground/SentryPlayground.Build.cs b/sample/Source/SentryPlayground/SentryPlayground.Build.cs index 5abaab464..35cb4df6c 100644 --- a/sample/Source/SentryPlayground/SentryPlayground.Build.cs +++ b/sample/Source/SentryPlayground/SentryPlayground.Build.cs @@ -1,7 +1,6 @@ // Copyright (c) 2025 Sentry. All Rights Reserved. using UnrealBuildTool; -using System.IO; public class SentryPlayground : ModuleRules { From 4c7547b6a3c2e7b576e1a0f98656f97a7f893ced Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 21 Nov 2025 14:10:06 +0200 Subject: [PATCH 22/52] Fix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1360e6981..9a1d6e7d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,6 +200,7 @@ jobs: unreal-version: ${{ matrix.unreal }} integration-test-android: + needs: [test-android] name: Android UE ${{ matrix.unreal }} secrets: inherit strategy: From f0bb43dc162cd91cc24b290606fe7ddf1de0b00b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 10:36:37 +0200 Subject: [PATCH 23/52] Fix typo --- sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index e5421c64e..74167b4ca 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -20,7 +20,7 @@ void USentryPlaygroundGameInstance::Init() FString CommandLine = FCommandLine::Get(); - UE_LOG(LogSentrySample, Display, TEXT("Startin app with commandline: %s\n"), *CommandLine); + UE_LOG(LogSentrySample, Display, TEXT("Starting app with commandline: %s\n"), *CommandLine); // Check for expected test parameters to decide between running integration tests // or launching the sample app with UI for manual testing From 5fea785a272b3ff09a0576dec9caf33ea1582caa Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 10:37:14 +0200 Subject: [PATCH 24/52] Limit android e2e tests to 2 parallel jobs --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a1d6e7d6..f81a757b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,6 +205,7 @@ jobs: secrets: inherit strategy: fail-fast: false + max-parallel: 2 matrix: unreal: ['5.4', '5.5', '5.6', '5.7'] uses: ./.github/workflows/integration-test-android.yml From 9eeb76980d6f2373e79405bf8563bc02d318554d Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 10:39:29 +0200 Subject: [PATCH 25/52] Remove Pester install step as it is redundant --- .github/workflows/integration-test-android.yml | 4 ---- .github/workflows/integration-test-linux.yml | 5 ----- 2 files changed, 9 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 97cb86769..be262aa1c 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -27,10 +27,6 @@ jobs: name: UE ${{ inputs.unreal-version }} sample build (Android) path: sample-build - - name: Install Pester - shell: pwsh - run: Install-Module -Name Pester -Force -SkipPublisherCheck - - name: Run integration tests id: run-integration-tests shell: pwsh diff --git a/.github/workflows/integration-test-linux.yml b/.github/workflows/integration-test-linux.yml index 66dfc856a..639dfe661 100644 --- a/.github/workflows/integration-test-linux.yml +++ b/.github/workflows/integration-test-linux.yml @@ -26,11 +26,6 @@ jobs: chmod +x ${{ github.workspace }}/sample-build/SentryPlayground.sh chmod +x ${{ github.workspace }}/sample-build/SentryPlayground/Plugins/sentry/Binaries/Linux/crashpad_handler - - name: Install Pester - shell: pwsh - run: | - Install-Module -Name Pester -Force -SkipPublisherCheck - - name: Run integration tests id: run-integration-tests shell: pwsh From aeb2f78eda2251962983d31355297d6e26f16c55 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 11:11:06 +0200 Subject: [PATCH 26/52] Rename test scripts --- .github/workflows/integration-test-android.yml | 2 +- ...ests.Android.Adb.ps1 => Integration.Android.Adb.Tests.ps1} | 0 ....SauceLabs.ps1 => Integration.Android.SauceLabs.Tests.ps1} | 0 integration-test/README.md | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) rename integration-test/{Integration.Tests.Android.Adb.ps1 => Integration.Android.Adb.Tests.ps1} (100%) rename integration-test/{Integration.Tests.Android.SauceLabs.ps1 => Integration.Android.SauceLabs.Tests.ps1} (100%) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index be262aa1c..3f68baf41 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -39,7 +39,7 @@ jobs: cd integration-test mkdir build cmake -B build -S . - Invoke-Pester Integration.Tests.Android.SauceLabs.ps1 -CI + Invoke-Pester Integration.Android.SauceLabs.Tests.ps1 -CI - name: Upload integration test output if: ${{ always() }} diff --git a/integration-test/Integration.Tests.Android.Adb.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 similarity index 100% rename from integration-test/Integration.Tests.Android.Adb.ps1 rename to integration-test/Integration.Android.Adb.Tests.ps1 diff --git a/integration-test/Integration.Tests.Android.SauceLabs.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 similarity index 100% rename from integration-test/Integration.Tests.Android.SauceLabs.ps1 rename to integration-test/Integration.Android.SauceLabs.Tests.ps1 diff --git a/integration-test/README.md b/integration-test/README.md index 53b855d85..47190c868 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -109,7 +109,7 @@ export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" # Run tests cd integration-test -pwsh -Command "Invoke-Pester Integration.Tests.Android.Adb.ps1" +pwsh -Command "Invoke-Pester ./Integration.Android.Adb.Tests.ps1" ``` ### Android (Cloud via SauceLabs) @@ -125,7 +125,7 @@ export SAUCE_REGION="eu-central-1" # Run tests cd integration-test -pwsh -Command "Invoke-Pester Integration.Tests.Android.SauceLabs.ps1" +pwsh -Command "Invoke-Pester ./Integration.Android.SauceLabs.Tests.ps1" ``` ## Test Coverage From c6777cc3255ff6cf5e074960e16fb41d17d91109 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 11:29:48 +0200 Subject: [PATCH 27/52] Clean up --- .github/workflows/integration-test-android.yml | 3 +-- .github/workflows/integration-test-linux.yml | 3 +-- .github/workflows/integration-test-windows.yml | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 3f68baf41..7c639c8c3 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -30,14 +30,13 @@ jobs: - name: Run integration tests id: run-integration-tests shell: pwsh + working-directory: integration-test env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-arm64.apk UNREAL_VERSION: ${{ inputs.unreal-version }} run: | - cd integration-test - mkdir build cmake -B build -S . Invoke-Pester Integration.Android.SauceLabs.Tests.ps1 -CI diff --git a/.github/workflows/integration-test-linux.yml b/.github/workflows/integration-test-linux.yml index 639dfe661..f81a6f44a 100644 --- a/.github/workflows/integration-test-linux.yml +++ b/.github/workflows/integration-test-linux.yml @@ -29,13 +29,12 @@ jobs: - name: Run integration tests id: run-integration-tests shell: pwsh + working-directory: integration-test env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground.sh run: | - cd integration-test - mkdir build cmake -B build -S . Invoke-Pester Integration.Tests.ps1 -CI diff --git a/.github/workflows/integration-test-windows.yml b/.github/workflows/integration-test-windows.yml index 71a72ff71..a528de0f3 100644 --- a/.github/workflows/integration-test-windows.yml +++ b/.github/workflows/integration-test-windows.yml @@ -30,13 +30,12 @@ jobs: - name: Run integration tests id: run-integration-tests + working-directory: integration-test env: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground.exe run: | - cd integration-test - mkdir build cmake -B build -S . Invoke-Pester Integration.Tests.ps1 -CI From d03ecbb5b82c175d193697a864d3011134fbd7e0 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 14:22:36 +0200 Subject: [PATCH 28/52] Rework SauceLabs API requests --- .../Integration.Android.SauceLabs.Tests.ps1 | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index 6ec5e115b..a471f708a 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -47,11 +47,12 @@ function script:Invoke-SauceLabsApi { try { if ($IsMultipart) { - # Use curl for multipart uploads (PowerShell's Invoke-WebRequest struggles with this) - $curlCmd = "curl -u `"$username`:$accessKey`" -X $Method `"$Uri`" -F `"payload=@$FilePath`" -F `"name=$(Split-Path $FilePath -Leaf)`"" - Write-Debug "Executing: $curlCmd" - $response = Invoke-Expression $curlCmd | ConvertFrom-Json - return $response + # Use -Form parameter for multipart uploads (PowerShell Core 7+) + $form = @{ + payload = Get-Item -Path $FilePath + name = (Split-Path $FilePath -Leaf) + } + $webResponse = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $headers -Form $form } else { $params = @{ Uri = $Uri @@ -64,17 +65,21 @@ function script:Invoke-SauceLabsApi { $params['ContentType'] = $ContentType } - $response = Invoke-RestMethod @params - return $response + $webResponse = Invoke-WebRequest @params + } + + # Explicit JSON parsing for better error visibility (Invoke-RestMethod can silently return strings) + if ($webResponse.Content) { + return $webResponse.Content | ConvertFrom-Json -AsHashtable } + return $null } catch { - Write-Error "SauceLabs API call failed: $($_.Exception.Message)" + $ErrorMessage = "SauceLabs API request ($Method $Uri) failed: $($_.Exception.Message)" if ($_.Exception.Response) { - $reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) - $responseBody = $reader.ReadToEnd() - Write-Error "Response: $responseBody" + $StatusCode = $_.Exception.Response.StatusCode + $ErrorMessage += " (Status: $StatusCode)" } - throw + throw $ErrorMessage } } From 30d3be26f697c44ece979043107dcd9d932c67a7 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 15:17:28 +0200 Subject: [PATCH 29/52] Simplify logs retrieval --- .../Integration.Android.SauceLabs.Tests.ps1 | 39 ++----------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index a471f708a..aa5209c0b 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -10,7 +10,6 @@ $ErrorActionPreference = 'Stop' # Script-level state for session management $script:SessionId = $null -$script:LogLineCount = 0 # Track log lines read so far (for delta) function script:Invoke-SauceLabsApi { param( @@ -251,24 +250,6 @@ function script:Invoke-SauceLabsApp { $completed = $true break } - - # Also check logs for completion markers - $logBody = @{ type = "logcat" } - $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody - - if ($logResponse.value -and $logResponse.value.Count -gt $script:LogLineCount) { - # Use array slicing instead of Select-Object -Skip (more reliable) - $newLogs = @($logResponse.value[$script:LogLineCount..($logResponse.value.Count - 1)]) - $logMessages = @($newLogs | ForEach-Object { if ($_) { $_.message } }) - - # Check for completion markers - if (($logMessages | Where-Object { $_ -match "TEST_RESULT:" }) -or - ($logMessages | Where-Object { $_ -match "Requesting app exit" })) { - Write-Host "Test completion detected in logs" -ForegroundColor Green - $completed = $true - break - } - } } catch { Write-Warning "Failed to query app state: $_" } @@ -280,29 +261,15 @@ function script:Invoke-SauceLabsApp { Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow } - # Retrieve final logs (delta from last read) + # Retrieve logs after app completion Write-Host "Retrieving logs..." -ForegroundColor Yellow $logBody = @{ type = "logcat" } $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody - # Extract new log lines (delta) [array]$allLogs = @() if ($logResponse.value -and $logResponse.value.Count -gt 0) { - $totalLogCount = $logResponse.value.Count - Write-Debug "Total logs in response: $totalLogCount, Previously read: $script:LogLineCount" - - if ($totalLogCount -gt $script:LogLineCount) { - # Get only new logs using array slicing (more reliable than Select-Object -Skip) - $allLogs = @($logResponse.value[$script:LogLineCount..($totalLogCount - 1)]) - Write-Host "Retrieved $($allLogs.Count) new log lines" -ForegroundColor Cyan - } else { - Write-Host "No new log lines since last read" -ForegroundColor Yellow - } - - # Update counter for next read - $script:LogLineCount = $totalLogCount - } else { - Write-Host "No logs available in response" -ForegroundColor Yellow + $allLogs = @($logResponse.value) + Write-Host "Retrieved $($allLogs.Count) log lines" -ForegroundColor Cyan } # Convert SauceLabs log format to text (matching adb output) From 532fa784e207ae9797689cd3b5d58500db4e8c35 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 24 Nov 2025 16:11:24 +0200 Subject: [PATCH 30/52] Remove unnecessary quotes --- integration-test/Integration.Android.Adb.Tests.ps1 | 4 ++-- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-test/Integration.Android.Adb.Tests.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 index 08e83dd57..93c450ffd 100644 --- a/integration-test/Integration.Android.Adb.Tests.ps1 +++ b/integration-test/Integration.Android.Adb.Tests.ps1 @@ -244,7 +244,7 @@ BeforeAll { # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $cmdlineCrashArgs = "-e cmdline '-crash-capture'" + # $cmdlineCrashArgs = "-e cmdline -crash-capture" # $global:AndroidCrashResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan @@ -256,7 +256,7 @@ BeforeAll { # TODO: use -SkipReinstall to preserve the crash state. Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - $cmdlineMessageArgs = "-e cmdline '-message-capture'" + $cmdlineMessageArgs = "-e cmdline -message-capture" $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index aa5209c0b..e1a3196d4 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -383,7 +383,7 @@ BeforeAll { # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $cmdlineCrashArgs = "-e cmdline '-crash-capture'" + # $cmdlineCrashArgs = "-e cmdline -crash-capture" # $global:SauceLabsCrashResult = Invoke-SauceLabsApp ` # -PackageName $script:PackageName ` # -ActivityName $script:ActivityName ` @@ -398,7 +398,7 @@ BeforeAll { # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - $cmdlineMessageArgs = "-e cmdline '-message-capture'" + $cmdlineMessageArgs = "-e cmdline -message-capture" $global:SauceLabsMessageResult = Invoke-SauceLabsApp ` -PackageName $script:PackageName ` -ActivityName $script:ActivityName ` From 3e09c206bb82d0b9c6525d041311f5426153322f Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 10:42:21 +0200 Subject: [PATCH 31/52] Configure new SauceLabs credentials --- .github/workflows/integration-test-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 7c639c8c3..029483791 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -14,7 +14,7 @@ jobs: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} GITHUB_TOKEN: ${{ github.token }} - SAUCE_REGION: eu-central-1 + SAUCE_REGION: us-west-1 steps: - uses: actions/checkout@v4 From 1c27be6ffb2e793b9249e0c1e6c732b865b868f1 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 11:32:47 +0200 Subject: [PATCH 32/52] Change test device --- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index e1a3196d4..f1cee2edb 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -361,7 +361,7 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "com.epicgames.unreal.GameActivity" - $script:DeviceName = "Samsung_Galaxy_S23_FE_free" + $script:DeviceName = "Samsung_Galaxy_S23_15_real_sjc1" $script:UnrealVersion = if ($env:UNREAL_VERSION) { $env:UNREAL_VERSION } else { "5.x" } # Upload APK to SauceLabs Storage From 3d8a9c7e0f3d32dec9f10f60e49e34f2946c9bee Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 11:34:24 +0200 Subject: [PATCH 33/52] Change SauceLabs test job name title --- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index f1cee2edb..617009628 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -143,7 +143,7 @@ function script:Initialize-SauceLabsSession { 'appium:noReset' = $true 'appium:autoLaunch' = $false 'sauce:options' = @{ - name = "$UnrealVersion Android Integration Test" + name = "UE $UnrealVersion Android Integration Test" appiumVersion = "latest" } } From 7dbcb789c4d8b26130cceb358b0730606e32781c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 11:44:57 +0200 Subject: [PATCH 34/52] Fix android test results upload check --- .github/workflows/integration-test-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 029483791..b2ae004b1 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -41,7 +41,7 @@ jobs: Invoke-Pester Integration.Android.SauceLabs.Tests.ps1 -CI - name: Upload integration test output - if: ${{ always() }} + if: ${{ always() && steps.run-integration-tests.outcome == 'failure' }} uses: actions/upload-artifact@v4 with: name: UE ${{ inputs.unreal-version }} integration test output (Android) From e4a7195e05813be6c6184772b0c9e94874720f5b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 11:53:28 +0200 Subject: [PATCH 35/52] Make SauceLabs device name configurable --- .github/workflows/integration-test-android.yml | 1 + .../Integration.Android.SauceLabs.Tests.ps1 | 12 ++++++++++-- integration-test/README.md | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index b2ae004b1..178ea26d9 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -15,6 +15,7 @@ jobs: SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} GITHUB_TOKEN: ${{ github.token }} SAUCE_REGION: us-west-1 + SAUCE_DEVICE_NAME: Samsung_Galaxy_S23_15_real_sjc1 steps: - uses: actions/checkout@v4 diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index 617009628..36e671521 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -3,7 +3,10 @@ # - Pre-built APK # - SauceLabs account credentials # - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH -# SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION +# SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME +# +# Note: SAUCE_DEVICE_NAME must match a device available in SAUCE_REGION. +# Example: For SAUCE_REGION=us-west-1, use devices with 'sjc1' suffix (San Jose DC1) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' @@ -319,6 +322,7 @@ BeforeAll { $script:SauceUsername = $env:SAUCE_USERNAME $script:SauceAccessKey = $env:SAUCE_ACCESS_KEY $script:SauceRegion = $env:SAUCE_REGION + $script:SauceDeviceName = $env:SAUCE_DEVICE_NAME if (-not $script:DSN) { throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" @@ -344,6 +348,10 @@ BeforeAll { throw "Environment variable SAUCE_REGION must be set" } + if (-not $script:SauceDeviceName) { + throw "Environment variable SAUCE_DEVICE_NAME must be set" + } + # Connect to Sentry API Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken @@ -361,7 +369,7 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "com.epicgames.unreal.GameActivity" - $script:DeviceName = "Samsung_Galaxy_S23_15_real_sjc1" + $script:DeviceName = $script:SauceDeviceName $script:UnrealVersion = if ($env:UNREAL_VERSION) { $env:UNREAL_VERSION } else { "5.x" } # Upload APK to SauceLabs Storage diff --git a/integration-test/README.md b/integration-test/README.md index 47190c868..05e8f346b 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -31,7 +31,17 @@ Supports testing on: - **Additional environment variables**: - `SAUCE_USERNAME` - SauceLabs username - `SAUCE_ACCESS_KEY` - SauceLabs access key - - `SAUCE_REGION` - SauceLabs region (e.g., `eu-central-1`) + - `SAUCE_REGION` - SauceLabs region (e.g., `us-west-1`, `eu-central-1`) + - `SAUCE_DEVICE_NAME` - Device name available in the specified region (must match region datacenter suffix) + +**Note**: The device name must match a device available in your SauceLabs region. Device names include a datacenter suffix that must align with the region: +- `us-west-1` → devices ending in `_sjc1` (San Jose DC1) +- `eu-central-1` → devices ending in `_fra1` (Frankfurt DC1) +- `us-east-4` → devices ending in `_use1` (US East DC1) + +Example valid combinations: +- Region: `us-west-1`, Device: `Samsung_Galaxy_S23_15_real_sjc1` ✓ +- Region: `eu-central-1`, Device: `Samsung_Galaxy_S23_15_real_sjc1` ✗ (mismatch) ## Setup @@ -121,13 +131,16 @@ export SENTRY_AUTH_TOKEN="sntrys_your_token_here" export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" export SAUCE_USERNAME="your-saucelabs-username" export SAUCE_ACCESS_KEY="your-saucelabs-access-key" -export SAUCE_REGION="eu-central-1" +export SAUCE_REGION="us-west-1" +export SAUCE_DEVICE_NAME="Samsung_Galaxy_S23_15_real_sjc1" # Run tests cd integration-test pwsh -Command "Invoke-Pester ./Integration.Android.SauceLabs.Tests.ps1" ``` +**Note**: Ensure `SAUCE_DEVICE_NAME` matches a device available in your `SAUCE_REGION`. See the [SauceLabs Platform Configurator](https://app.saucelabs.com/live/web-testing) to find available devices for your region. + ## Test Coverage The integration tests cover: From e43d13f5757a84bb4e3eee3f9539ef2890cc3080 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 11:58:40 +0200 Subject: [PATCH 36/52] Fix sample app --- .../SentryPlaygroundGameInstance.cpp | 14 +++++++------- .../SentryPlaygroundGameInstance.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index 74167b4ca..9a525c350 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -27,13 +27,13 @@ void USentryPlaygroundGameInstance::Init() if (FParse::Param(*CommandLine, TEXT("crash-capture")) || FParse::Param(*CommandLine, TEXT("message-capture"))) { - RunIntegrationTest(*CommandLine); + RunIntegrationTest(CommandLine); } } -void USentryPlaygroundGameInstance::RunIntegrationTest(const TCHAR* CommandLine) +void USentryPlaygroundGameInstance::RunIntegrationTest(const FString& CommandLine) { - UE_LOG(LogSentrySample, Display, TEXT("Running integration test for command: %s\n"), CommandLine); + UE_LOG(LogSentrySample, Display, TEXT("Running integration test for command: %s\n"), *CommandLine); USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem(); if (!SentrySubsystem) @@ -42,11 +42,11 @@ void USentryPlaygroundGameInstance::RunIntegrationTest(const TCHAR* CommandLine) return; } - SentrySubsystem->InitializeWithSettings(FConfigureSettingsNativeDelegate::CreateLambda([=](USentrySettings* Settings) + SentrySubsystem->InitializeWithSettings(FConfigureSettingsNativeDelegate::CreateLambda([CommandLine](USentrySettings* Settings) { // Override options set in config file if needed FString Dsn; - if (FParse::Value(CommandLine, TEXT("dsn="), Dsn)) + if (FParse::Value(*CommandLine, TEXT("dsn="), Dsn)) { Settings->Dsn = Dsn; } @@ -66,11 +66,11 @@ void USentryPlaygroundGameInstance::RunIntegrationTest(const TCHAR* CommandLine) SentrySubsystem->AddBreadcrumbWithParams( TEXT("Context configuration finished"), TEXT("Test"), TEXT("info"), TMap(), ESentryLevel::Info); - if (FParse::Param(CommandLine, TEXT("crash-capture"))) + if (FParse::Param(*CommandLine, TEXT("crash-capture"))) { RunCrashTest(); } - else if (FParse::Param(CommandLine, TEXT("message-capture"))) + else if (FParse::Param(*CommandLine, TEXT("message-capture"))) { RunMessageTest(); } diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h index 636baeda3..0a5a6b0c8 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h @@ -20,7 +20,7 @@ class SENTRYPLAYGROUND_API USentryPlaygroundGameInstance : public UGameInstance virtual void Init() override; private: - void RunIntegrationTest(const TCHAR* CommandLine); + void RunIntegrationTest(const FString& CommandLine); void RunCrashTest(); void RunMessageTest(); From c218ae8251bb8e078c9354ccb47ef6fb06a00a93 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 16:47:26 +0200 Subject: [PATCH 37/52] Try using app-runner Android providers for running tests --- integration-test/CMakeLists.txt | 2 +- .../Integration.Android.Adb.Tests.ps1 | 194 +--------- .../Integration.Android.SauceLabs.Tests.ps1 | 343 +----------------- 3 files changed, 30 insertions(+), 509 deletions(-) diff --git a/integration-test/CMakeLists.txt b/integration-test/CMakeLists.txt index ec54093bc..566786492 100644 --- a/integration-test/CMakeLists.txt +++ b/integration-test/CMakeLists.txt @@ -7,7 +7,7 @@ include(FetchContent) FetchContent_Declare( app-runner GIT_REPOSITORY https://github.com/getsentry/app-runner.git - GIT_TAG 503795f0ef0f8340fcc0f0bc5fb5437df8cff9ef + GIT_TAG b1d7d0f97959f1ba7b1d52682d45ee9adf3adf96 ) FetchContent_MakeAvailable(app-runner) diff --git a/integration-test/Integration.Android.Adb.Tests.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 index 93c450ffd..be3475461 100644 --- a/integration-test/Integration.Android.Adb.Tests.ps1 +++ b/integration-test/Integration.Android.Adb.Tests.ps1 @@ -1,4 +1,4 @@ -# Integration tests for Sentry Unreal SDK on Android +# Integration tests for Sentry Unreal SDK on Android via ADB # Requires: # - Pre-built APK (x64 for emulator) # - Android emulator or device connected @@ -7,180 +7,6 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -function script:Get-AndroidDeviceId { - $lines = adb devices | Select-String "device$" - - if (-not $lines) { - throw "No Android devices found. Is emulator running?" - } - - # Extract device ID from the first matching line - # Line format: "emulator-5554 device" - $firstLine = $lines | Select-Object -First 1 - $deviceId = ($firstLine.Line -split '\s+')[0] - - if (-not $deviceId) { - throw "Could not extract device ID from: $($firstLine.Line)" - } - - return $deviceId -} - -function script:Install-AndroidApp { - param( - [Parameter(Mandatory)] - [string]$ApkPath, - - [Parameter(Mandatory)] - [string]$PackageName, - - [Parameter(Mandatory)] - [string]$DeviceId - ) - - if (-not (Test-Path $ApkPath)) { - throw "APK file not found: $ApkPath" - } - - if ($ApkPath -notlike '*.apk') { - throw "Package must be an .apk file. Got: $ApkPath" - } - - # Check for existing installation - Write-Debug "Checking for existing package: $PackageName" - $installed = adb -s $DeviceId shell pm list packages | Select-String -Pattern $PackageName -SimpleMatch - - if ($installed) { - Write-Host "Uninstalling previous version..." -ForegroundColor Yellow - adb -s $DeviceId uninstall $PackageName | Out-Null - Start-Sleep -Seconds 1 - } - - # Install APK - Write-Host "Installing APK to device: $DeviceId" -ForegroundColor Yellow - $installOutput = adb -s $DeviceId install -r $ApkPath 2>&1 | Out-String - - if ($LASTEXITCODE -ne 0 -or $installOutput -notmatch "Success") { - throw "Failed to install APK (exit code: $LASTEXITCODE): $installOutput" - } - - Write-Host "Package installed successfully: $PackageName" -ForegroundColor Green -} - -function script:Invoke-AndroidApp { - param( - [Parameter(Mandatory)] - [string]$ExecutablePath, - - [Parameter()] - [string]$Arguments = "" - ) - - # Extract package name from activity path (format: package.name/activity.name) - if ($ExecutablePath -notmatch '^([^/]+)/') { - throw "ExecutablePath must be in format 'package.name/activity.name'. Got: $ExecutablePath" - } - $packageName = $matches[1] - - # Use script-level Android configuration - $deviceId = $script:DeviceId - $outputDir = $script:OutputDir - - # Android-specific timeout configuration - $timeoutSeconds = 300 - $initialWaitSeconds = 3 - $pidRetrySeconds = 30 - $logPollIntervalSeconds = 2 - - $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $logFile = if ($OutputDir) { "$OutputDir/$timestamp-logcat.txt" } else { $null } - - # Clear logcat before launch - Write-Debug "Clearing logcat on device: $deviceId" - adb -s $deviceId logcat -c - - # Launch activity with Intent extras - Write-Host "Launching: $ExecutablePath" -ForegroundColor Cyan - if ($Arguments) { - Write-Host " Arguments: $Arguments" -ForegroundColor Cyan - } - - $startOutput = adb -s $deviceId shell am start -n $ExecutablePath $Arguments -W 2>&1 | Out-String - - if ($startOutput -match "Error") { - throw "Failed to start activity: $startOutput" - } - - # Get process ID (with retries) - Write-Debug "Waiting for app process..." - Start-Sleep -Seconds $initialWaitSeconds - - $appPID = $null - for ($i = 0; $i -lt $pidRetrySeconds; $i++) { - $pidOutput = adb -s $deviceId shell pidof $packageName 2>&1 - if ($pidOutput) { - $pidOutput = $pidOutput.ToString().Trim() - if ($pidOutput -match '^\d+$') { - $appPID = $pidOutput - break - } - } - Start-Sleep -Seconds 1 - } - - # Initialize log cache as array for consistent type handling - [array]$logCache = @() - - if (-not $appPID) { - # App might have already exited (fast message test) - capture logs anyway - Write-Host "Warning: Could not find process ID (app may have exited quickly)" -ForegroundColor Yellow - $logCache = @(adb -s $deviceId logcat -d 2>&1) - $exitCode = 0 - } else { - Write-Host "App PID: $appPID" -ForegroundColor Green - - # Monitor logcat for test completion - $startTime = Get-Date - $completed = $false - - while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($timeoutSeconds)) { - $newLogs = adb -s $deviceId logcat -d --pid=$appPID 2>&1 - if ($newLogs) { - $logCache = @($newLogs) - - # Check for completion markers from SentryPlaygroundGameInstance - if (($newLogs | Select-String "TEST_RESULT:") -or - ($newLogs | Select-String "Requesting app exit")) { - $completed = $true - Write-Host "Test completion detected" -ForegroundColor Green - break - } - } - - Start-Sleep -Seconds $logPollIntervalSeconds - } - - if (-not $completed) { - Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow - } - - $exitCode = 0 # Android apps don't report exit codes via adb - } - - # Save logcat to file if OutputDir specified - if ($logFile) { - $logCache | Out-File $logFile - Write-Host "Logcat saved to: $logFile" - } - - # Return structured result (matches app-runner pattern) - return @{ - ExitCode = $exitCode - Output = $logCache - Error = @() - } -} - BeforeAll { # Check if configuration file exists $configFile = "$PSScriptRoot/TestConfig.local.ps1" @@ -229,13 +55,13 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - # Get Android device - $script:DeviceId = Get-AndroidDeviceId - Write-Host "Found Android device: $script:DeviceId" -ForegroundColor Green + # Connect to Android device via ADB (auto-discovers available device) + Write-Host "Connecting to Android device..." -ForegroundColor Yellow + Connect-Device -Platform AndroidAdb # Install APK to device Write-Host "Installing APK to Android device..." -ForegroundColor Yellow - Install-AndroidApp -ApkPath $script:ApkPath -PackageName $script:PackageName -DeviceId $script:DeviceId + Install-DeviceApp -PackagePath $script:ApkPath # ========================================== # RUN 1: Crash test - creates minidump @@ -245,7 +71,7 @@ BeforeAll { # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow # $cmdlineCrashArgs = "-e cmdline -crash-capture" - # $global:AndroidCrashResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs + # $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan @@ -257,7 +83,7 @@ BeforeAll { Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow $cmdlineMessageArgs = "-e cmdline -message-capture" - $global:AndroidMessageResult = Invoke-AndroidApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs + $global:AndroidMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan } @@ -412,7 +238,13 @@ Describe "Sentry Unreal Android Integration Tests" { } AfterAll { + # Disconnect from Android device + Write-Host "Disconnecting from Android device..." -ForegroundColor Yellow + Disconnect-Device + + # Disconnect from Sentry API Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow Disconnect-SentryApi + Write-Host "Integration tests complete" -ForegroundColor Green } \ No newline at end of file diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index 36e671521..c7277c2cd 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -11,297 +11,6 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -# Script-level state for session management -$script:SessionId = $null - -function script:Invoke-SauceLabsApi { - param( - [Parameter(Mandatory)] - [string]$Method, - - [Parameter(Mandatory)] - [string]$Uri, - - [Parameter()] - [hashtable]$Body = $null, - - [Parameter()] - [string]$ContentType = 'application/json', - - [Parameter()] - [switch]$IsMultipart, - - [Parameter()] - [string]$FilePath - ) - - $username = $env:SAUCE_USERNAME - $accessKey = $env:SAUCE_ACCESS_KEY - - if (-not $username -or -not $accessKey) { - throw "SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables must be set" - } - - $base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${username}:${accessKey}")) - $headers = @{ - 'Authorization' = "Basic $base64Auth" - } - - try { - if ($IsMultipart) { - # Use -Form parameter for multipart uploads (PowerShell Core 7+) - $form = @{ - payload = Get-Item -Path $FilePath - name = (Split-Path $FilePath -Leaf) - } - $webResponse = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $headers -Form $form - } else { - $params = @{ - Uri = $Uri - Method = $Method - Headers = $headers - } - - if ($Body) { - $params['Body'] = ($Body | ConvertTo-Json -Depth 10) - $params['ContentType'] = $ContentType - } - - $webResponse = Invoke-WebRequest @params - } - - # Explicit JSON parsing for better error visibility (Invoke-RestMethod can silently return strings) - if ($webResponse.Content) { - return $webResponse.Content | ConvertFrom-Json -AsHashtable - } - return $null - } catch { - $ErrorMessage = "SauceLabs API request ($Method $Uri) failed: $($_.Exception.Message)" - if ($_.Exception.Response) { - $StatusCode = $_.Exception.Response.StatusCode - $ErrorMessage += " (Status: $StatusCode)" - } - throw $ErrorMessage - } -} - -function script:Install-SauceLabsApp { - param( - [Parameter(Mandatory)] - [string]$ApkPath, - - [Parameter(Mandatory)] - [string]$Region - ) - - if (-not (Test-Path $ApkPath)) { - throw "APK file not found: $ApkPath" - } - - if ($ApkPath -notlike '*.apk') { - throw "Package must be an .apk file. Got: $ApkPath" - } - - Write-Host "Uploading APK to SauceLabs Storage..." -ForegroundColor Yellow - $uploadUri = "https://api.${Region}.saucelabs.com/v1/storage/upload" - - $response = Invoke-SauceLabsApi -Method POST -Uri $uploadUri -IsMultipart -FilePath $ApkPath - - if (-not $response.item.id) { - throw "Failed to upload APK: No storage ID in response" - } - - $storageId = $response.item.id - Write-Host "APK uploaded successfully. Storage ID: $storageId" -ForegroundColor Green - - return $storageId -} - -function script:Initialize-SauceLabsSession { - param( - [Parameter(Mandatory)] - [string]$StorageId, - - [Parameter(Mandatory)] - [string]$DeviceName, - - [Parameter(Mandatory)] - [string]$Region, - - [Parameter(Mandatory)] - [string]$UnrealVersion - ) - - Write-Host "Creating SauceLabs Appium session..." -ForegroundColor Yellow - - $sessionUri = "https://ondemand.${Region}.saucelabs.com/wd/hub/session" - - $capabilities = @{ - capabilities = @{ - alwaysMatch = @{ - platformName = "Android" - 'appium:app' = "storage:$StorageId" - 'appium:deviceName' = $DeviceName - 'appium:automationName' = "UiAutomator2" - 'appium:noReset' = $true - 'appium:autoLaunch' = $false - 'sauce:options' = @{ - name = "UE $UnrealVersion Android Integration Test" - appiumVersion = "latest" - } - } - } - } - - $response = Invoke-SauceLabsApi -Method POST -Uri $sessionUri -Body $capabilities - - $sessionId = $response.value.sessionId - if (-not $sessionId) { - $sessionId = $response.sessionId - } - - if (-not $sessionId) { - throw "Failed to create session: No session ID in response" - } - - Write-Host "Session created successfully. Session ID: $sessionId" -ForegroundColor Green - - return $sessionId -} - -function script:Invoke-SauceLabsApp { - param( - [Parameter(Mandatory)] - [string]$PackageName, - - [Parameter(Mandatory)] - [string]$ActivityName, - - [Parameter()] - [string]$Arguments = "", - - [Parameter(Mandatory)] - [string]$Region - ) - - $sessionId = $script:SessionId - if (-not $sessionId) { - throw "No active SauceLabs session. Call Initialize-SauceLabsSession first." - } - - $baseUri = "https://ondemand.${Region}.saucelabs.com/wd/hub/session/$sessionId" - $outputDir = $script:OutputDir - - # Configuration - $timeoutSeconds = 300 - $pollIntervalSeconds = 2 - - $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $logFile = if ($OutputDir) { "$OutputDir/$timestamp-logcat.txt" } else { $null } - - # Launch activity with Intent extras - Write-Host "Launching: $PackageName/$ActivityName" -ForegroundColor Cyan - if ($Arguments) { - Write-Host " Arguments: $Arguments" -ForegroundColor Cyan - } - - $launchBody = @{ - appPackage = $PackageName - appActivity = $ActivityName - appWaitActivity = "*" - intentAction = "android.intent.action.MAIN" - intentCategory = "android.intent.category.LAUNCHER" - } - - if ($Arguments) { - $launchBody['optionalIntentArguments'] = $Arguments - } - - try { - $launchResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/appium/device/start_activity" -Body $launchBody - Write-Debug "Launch response: $($launchResponse | ConvertTo-Json)" - } catch { - throw "Failed to launch activity: $_" - } - - # Wait a moment for app to start - Start-Sleep -Seconds 3 - - # Poll app state until it exits or completes - Write-Host "Monitoring app execution..." -ForegroundColor Yellow - $startTime = Get-Date - $completed = $false - - while ((Get-Date) - $startTime -lt [TimeSpan]::FromSeconds($timeoutSeconds)) { - # Query app state - $stateBody = @{ - script = "mobile: queryAppState" - args = @( - @{ appId = $PackageName } - ) - } - - try { - $stateResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/execute/sync" -Body $stateBody - $appState = $stateResponse.value - - Write-Debug "App state: $appState (elapsed: $([int]((Get-Date) - $startTime).TotalSeconds)s)" - - # State 1 = not running, 0 = not installed - if ($appState -eq 1 -or $appState -eq 0) { - Write-Host "App finished/crashed (state: $appState)" -ForegroundColor Green - $completed = $true - break - } - } catch { - Write-Warning "Failed to query app state: $_" - } - - Start-Sleep -Seconds $pollIntervalSeconds - } - - if (-not $completed) { - Write-Host "Warning: Test did not complete within timeout" -ForegroundColor Yellow - } - - # Retrieve logs after app completion - Write-Host "Retrieving logs..." -ForegroundColor Yellow - $logBody = @{ type = "logcat" } - $logResponse = Invoke-SauceLabsApi -Method POST -Uri "$baseUri/log" -Body $logBody - - [array]$allLogs = @() - if ($logResponse.value -and $logResponse.value.Count -gt 0) { - $allLogs = @($logResponse.value) - Write-Host "Retrieved $($allLogs.Count) log lines" -ForegroundColor Cyan - } - - # Convert SauceLabs log format to text (matching adb output) - $logCache = @() - if ($allLogs -and $allLogs.Count -gt 0) { - $logCache = $allLogs | ForEach-Object { - if ($_) { - $timestamp = if ($_.timestamp) { $_.timestamp } else { "" } - $level = if ($_.level) { $_.level } else { "" } - $message = if ($_.message) { $_.message } else { "" } - "$timestamp $level $message" - } - } | Where-Object { $_ } # Filter out any nulls - } - - # Save logs to file if OutputDir specified - if ($logFile) { - $logCache | Out-File $logFile - Write-Host "Logs saved to: $logFile" -ForegroundColor Cyan - } - - # Return structured result (matches app-runner pattern) - return @{ - ExitCode = 0 # Android apps don't report exit codes - Output = $logCache - Error = @() - } -} - BeforeAll { # Check if configuration file exists $configFile = "$PSScriptRoot/TestConfig.local.ps1" @@ -368,21 +77,15 @@ BeforeAll { } $script:PackageName = "io.sentry.unreal.sample" - $script:ActivityName = "com.epicgames.unreal.GameActivity" - $script:DeviceName = $script:SauceDeviceName - $script:UnrealVersion = if ($env:UNREAL_VERSION) { $env:UNREAL_VERSION } else { "5.x" } - - # Upload APK to SauceLabs Storage - Write-Host "Uploading APK to SauceLabs..." -ForegroundColor Yellow - $script:StorageId = Install-SauceLabsApp -ApkPath $script:ApkPath -Region $script:SauceRegion - - # Create SauceLabs session (reused for all app launches) - Write-Host "Creating SauceLabs session..." -ForegroundColor Yellow - $script:SessionId = Initialize-SauceLabsSession ` - -StorageId $script:StorageId ` - -DeviceName $script:DeviceName ` - -Region $script:SauceRegion ` - -UnrealVersion $script:UnrealVersion + $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + + # Connect to SauceLabs (reads credentials and configuration from environment variables) + Write-Host "Connecting to SauceLabs..." -ForegroundColor Yellow + Connect-Device -Platform AndroidSauceLabs + + # Install APK to SauceLabs device + Write-Host "Installing APK to SauceLabs device..." -ForegroundColor Yellow + Install-DeviceApp -PackagePath $script:ApkPath # ========================================== # RUN 1: Crash test - creates minidump @@ -392,11 +95,7 @@ BeforeAll { # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow # $cmdlineCrashArgs = "-e cmdline -crash-capture" - # $global:SauceLabsCrashResult = Invoke-SauceLabsApp ` - # -PackageName $script:PackageName ` - # -ActivityName $script:ActivityName ` - # -Arguments $cmdlineCrashArgs ` - # -Region $script:SauceRegion + # $global:SauceLabsCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs # Write-Host "Crash test exit code: $($global:SauceLabsCrashResult.ExitCode)" -ForegroundColor Cyan @@ -407,11 +106,7 @@ BeforeAll { Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow $cmdlineMessageArgs = "-e cmdline -message-capture" - $global:SauceLabsMessageResult = Invoke-SauceLabsApp ` - -PackageName $script:PackageName ` - -ActivityName $script:ActivityName ` - -Arguments $cmdlineMessageArgs ` - -Region $script:SauceRegion + $global:SauceLabsMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs Write-Host "Message test exit code: $($global:SauceLabsMessageResult.ExitCode)" -ForegroundColor Cyan } @@ -566,19 +261,13 @@ Describe "Sentry Unreal Android Integration Tests (SauceLabs)" { } AfterAll { - # Clean up SauceLabs session - if ($script:SessionId) { - Write-Host "Ending SauceLabs session..." -ForegroundColor Yellow - try { - $sessionUri = "https://ondemand.$($script:SauceRegion).saucelabs.com/wd/hub/session/$($script:SessionId)" - Invoke-SauceLabsApi -Method DELETE -Uri $sessionUri - Write-Host "Session ended successfully" -ForegroundColor Green - } catch { - Write-Warning "Failed to end session: $_" - } - } + # Disconnect from SauceLabs device (cleans up session) + Write-Host "Disconnecting from SauceLabs device..." -ForegroundColor Yellow + Disconnect-Device + # Disconnect from Sentry API Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow Disconnect-SentryApi + Write-Host "Integration tests complete" -ForegroundColor Green } From 25fe6150ee9165ae23641abea1c611a152cea88f Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 17:48:16 +0200 Subject: [PATCH 38/52] Fix param name --- integration-test/Integration.Android.Adb.Tests.ps1 | 2 +- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-test/Integration.Android.Adb.Tests.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 index be3475461..6b65a054c 100644 --- a/integration-test/Integration.Android.Adb.Tests.ps1 +++ b/integration-test/Integration.Android.Adb.Tests.ps1 @@ -61,7 +61,7 @@ BeforeAll { # Install APK to device Write-Host "Installing APK to Android device..." -ForegroundColor Yellow - Install-DeviceApp -PackagePath $script:ApkPath + Install-DeviceApp -Path $script:ApkPath # ========================================== # RUN 1: Crash test - creates minidump diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index c7277c2cd..ca33f7a7e 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -85,7 +85,7 @@ BeforeAll { # Install APK to SauceLabs device Write-Host "Installing APK to SauceLabs device..." -ForegroundColor Yellow - Install-DeviceApp -PackagePath $script:ApkPath + Install-DeviceApp -Path $script:ApkPath # ========================================== # RUN 1: Crash test - creates minidump From 69f3e65ef3fe54f14eb61c923cbff6c38f489d04 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 25 Nov 2025 19:54:29 +0200 Subject: [PATCH 39/52] Pass device name to SauceLabs provider --- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index ca33f7a7e..0d57c170f 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -79,9 +79,9 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - # Connect to SauceLabs (reads credentials and configuration from environment variables) + # Connect to SauceLabs (reads credentials and region from environment variables) Write-Host "Connecting to SauceLabs..." -ForegroundColor Yellow - Connect-Device -Platform AndroidSauceLabs + Connect-Device -Platform AndroidSauceLabs -Target $script:SauceDeviceName # Install APK to SauceLabs device Write-Host "Installing APK to SauceLabs device..." -ForegroundColor Yellow From 0b5fa5f2dfa44749da8696ef229843a9a72497ea Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 09:37:49 +0200 Subject: [PATCH 40/52] Connect to SauceLabs device set in env var --- integration-test/Integration.Android.Adb.Tests.ps1 | 3 +-- integration-test/Integration.Android.SauceLabs.Tests.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/integration-test/Integration.Android.Adb.Tests.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 index 6b65a054c..74ad2e04c 100644 --- a/integration-test/Integration.Android.Adb.Tests.ps1 +++ b/integration-test/Integration.Android.Adb.Tests.ps1 @@ -1,6 +1,6 @@ # Integration tests for Sentry Unreal SDK on Android via ADB # Requires: -# - Pre-built APK (x64 for emulator) +# - Pre-built APK (x64 for emulator, arm64 for device) # - Android emulator or device connected # - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH @@ -79,7 +79,6 @@ BeforeAll { # RUN 2: Message test - uploads crash from Run 1 + captures message # ========================================== # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. - # TODO: use -SkipReinstall to preserve the crash state. Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow $cmdlineMessageArgs = "-e cmdline -message-capture" diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 index 0d57c170f..b6b4d627e 100644 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ b/integration-test/Integration.Android.SauceLabs.Tests.ps1 @@ -79,9 +79,9 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - # Connect to SauceLabs (reads credentials and region from environment variables) + # Connect to SauceLabs (reads all configuration from environment variables) Write-Host "Connecting to SauceLabs..." -ForegroundColor Yellow - Connect-Device -Platform AndroidSauceLabs -Target $script:SauceDeviceName + Connect-Device -Platform AndroidSauceLabs # Install APK to SauceLabs device Write-Host "Installing APK to SauceLabs device..." -ForegroundColor Yellow From 3b20dc85de1b1add11b82399edda9132e6e74d57 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 10:37:23 +0200 Subject: [PATCH 41/52] Bump app-runner --- integration-test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/CMakeLists.txt b/integration-test/CMakeLists.txt index 566786492..0ede738bc 100644 --- a/integration-test/CMakeLists.txt +++ b/integration-test/CMakeLists.txt @@ -7,7 +7,7 @@ include(FetchContent) FetchContent_Declare( app-runner GIT_REPOSITORY https://github.com/getsentry/app-runner.git - GIT_TAG b1d7d0f97959f1ba7b1d52682d45ee9adf3adf96 + GIT_TAG 4a55e0a77c85b88cc3fd8b97724b473728d8aeaa ) FetchContent_MakeAvailable(app-runner) From c22b2c6ff59a0f10348adc4fa19c120a8ab5897a Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 11:56:27 +0200 Subject: [PATCH 42/52] Merge android test scripts --- .../workflows/integration-test-android.yml | 3 +- .../Integration.Android.Tests.ps1 | 280 ++++++++++++++++++ integration-test/README.md | 35 +-- 3 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 integration-test/Integration.Android.Tests.ps1 diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 178ea26d9..f83a831e5 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -39,7 +39,8 @@ jobs: UNREAL_VERSION: ${{ inputs.unreal-version }} run: | cmake -B build -S . - Invoke-Pester Integration.Android.SauceLabs.Tests.ps1 -CI + $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } + Invoke-Pester -Container $Container -CI - name: Upload integration test output if: ${{ always() && steps.run-integration-tests.outcome == 'failure' }} diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 new file mode 100644 index 000000000..6d450dc7c --- /dev/null +++ b/integration-test/Integration.Android.Tests.ps1 @@ -0,0 +1,280 @@ +# Integration tests for Sentry Unreal SDK on Android +# Supports both ADB (local devices/emulators) and SauceLabs (cloud devices) +# +# Usage: +# # Default (uses ADB) +# Invoke-Pester Integration.Android.Tests.ps1 +# +# # Explicit platform selection with Pester containers +# $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'Adb' } +# Invoke-Pester -Container $Container +# +# $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } +# Invoke-Pester -Container $Container +# +# Requires: +# - Pre-built APK +# - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH +# +# For ADB: +# - Android emulator or device connected via ADB +# +# For SauceLabs: +# - SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME + +param( + [Parameter(Mandatory = $false)] + [ValidateSet('Adb', 'SauceLabs')] + [string]$Platform = 'Adb' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +BeforeAll { + # Map friendly platform name to app-runner platform name + $appRunnerPlatform = if ($Platform -eq 'Adb') { 'AndroidAdb' } else { 'AndroidSauceLabs' } + + Write-Host "Running Android tests with platform: $Platform (app-runner: $appRunnerPlatform)" -ForegroundColor Cyan + + # Check if configuration file exists + $configFile = "$PSScriptRoot/TestConfig.local.ps1" + if (-not (Test-Path $configFile)) { + throw "Configuration file '$configFile' not found. Run 'cmake -B build -S .' first" + } + + # Load configuration (provides $global:AppRunnerPath) + . $configFile + + # Import app-runner modules (SentryApiClient, test utilities) + . "$global:AppRunnerPath/import-modules.ps1" + + # Validate environment variables (test-specific only, not provider-specific) + $script:DSN = $env:SENTRY_UNREAL_TEST_DSN + $script:AuthToken = $env:SENTRY_AUTH_TOKEN + $script:ApkPath = $env:SENTRY_UNREAL_TEST_APP_PATH + + if (-not $script:DSN) { + throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" + } + + if (-not $script:AuthToken) { + throw "Environment variable SENTRY_AUTH_TOKEN must be set" + } + + if (-not $script:ApkPath) { + throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" + } + + # Validate app path + if (-not (Test-Path $script:ApkPath)) { + throw "Application not found at: $script:ApkPath" + } + + # Connect to Sentry API + Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow + Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken + + # Create output directory + $script:OutputDir = "$PSScriptRoot/output" + if (-not (Test-Path $script:OutputDir)) { + New-Item -ItemType Directory -Path $script:OutputDir | Out-Null + } + + $script:PackageName = "io.sentry.unreal.sample" + $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" + + # Connect to Android device (provider validates its own env vars) + Write-Host "Connecting to Android via $Platform..." -ForegroundColor Yellow + Connect-Device -Platform $appRunnerPlatform + + # Install APK + Write-Host "Installing APK via $Platform..." -ForegroundColor Yellow + Install-DeviceApp -Path $script:ApkPath + + # ========================================== + # RUN 1: Crash test - creates minidump + # ========================================== + # The crash is captured but NOT uploaded yet (Android behavior). + # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) + + # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow + # $cmdlineCrashArgs = "-e cmdline -crash-capture" + # $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs + + # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + + # ========================================== + # RUN 2: Message test - uploads crash from Run 1 + captures message + # ========================================== + # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. + + Write-Host "Running message-capture test on $Platform..." -ForegroundColor Yellow + $cmdlineMessageArgs = "-e cmdline -message-capture" + $global:AndroidMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs + + Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan +} + +Describe "Sentry Unreal Android Integration Tests ($Platform)" { + + # ========================================== + # NOTE: Crash Capture Tests are DISABLED due to tag sync issue + # Uncomment when Android SDK tag persistence is fixed + # ========================================== + # Context "Crash Capture Tests" { + # BeforeAll { + # # Crash event is sent during the MESSAGE run (Run 2) + # # But the crash_id comes from the CRASH run (Run 1) + # $CrashResult = $global:AndroidCrashResult + # $CrashEvent = $null + # + # # Parse crash event ID from crash run output + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + # + # if ($eventIds -and $eventIds.Count -gt 0) { + # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan + # $crashId = $eventIds[0] + # + # # Fetch crash event using the tag (event was sent during message run) + # try { + # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green + # } catch { + # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red + # } + # } else { + # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow + # } + # } + # + # It "Should output event ID before crash" { + # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + # $eventIds | Should -Not -BeNullOrEmpty + # $eventIds.Count | Should -Be 1 + # } + # + # It "Should capture crash event in Sentry (uploaded during next run)" { + # $CrashEvent | Should -Not -BeNullOrEmpty + # } + # + # It "Should have correct event type and platform" { + # $CrashEvent.type | Should -Be 'error' + # $CrashEvent.platform | Should -Be 'native' + # } + # + # It "Should have exception information" { + # $CrashEvent.exception | Should -Not -BeNullOrEmpty + # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty + # } + # + # It "Should have stack trace" { + # $exception = $CrashEvent.exception.values[0] + # $exception.stacktrace | Should -Not -BeNullOrEmpty + # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty + # } + # + # It "Should have user context" { + # $CrashEvent.user | Should -Not -BeNullOrEmpty + # $CrashEvent.user.username | Should -Be 'TestUser' + # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' + # $CrashEvent.user.id | Should -Be '12345' + # } + # + # It "Should have test.crash_id tag for correlation" { + # $tags = $CrashEvent.tags + # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } + # $crashIdTag | Should -Not -BeNullOrEmpty + # $crashIdTag.value | Should -Not -BeNullOrEmpty + # } + # + # It "Should have integration test tag" { + # $tags = $CrashEvent.tags + # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + # } + # + # It "Should have breadcrumbs from before crash" { + # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + # } + # } + + Context "Message Capture Tests" { + BeforeAll { + $MessageResult = $global:AndroidMessageResult + $MessageEvent = $null + + # Parse event ID from output + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 + + if ($eventIds -and $eventIds.Count -gt 0) { + Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan + + # Fetch event from Sentry (with polling) + try { + $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] + Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green + } + catch { + Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red + } + } + else { + Write-Host "Warning: No message event ID found in output" -ForegroundColor Yellow + } + } + + It "Should output event ID" { + $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 + $eventIds | Should -Not -BeNullOrEmpty + $eventIds.Count | Should -Be 1 + } + + It "Should output TEST_RESULT with success" { + $testResultLine = $MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine | Should -Not -BeNullOrEmpty + $testResultLine | Should -Match '"success"\s*:\s*true' + } + + It "Should capture message event in Sentry" { + $MessageEvent | Should -Not -BeNullOrEmpty + } + + It "Should have correct platform" { + # Android events are captured from Java layer, so platform is 'java' not 'native' + $MessageEvent.platform | Should -Be 'java' + } + + It "Should have message content" { + $MessageEvent.message | Should -Not -BeNullOrEmpty + $MessageEvent.message.formatted | Should -Match 'Integration test message' + } + + It "Should have user context" { + $MessageEvent.user | Should -Not -BeNullOrEmpty + $MessageEvent.user.username | Should -Be 'TestUser' + } + + It "Should have integration test tag" { + $tags = $MessageEvent.tags + ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + } + + It "Should have breadcrumbs" { + $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } +} + +AfterAll { + # Disconnect from Android device + Write-Host "Disconnecting from $Platform..." -ForegroundColor Yellow + Disconnect-Device + + # Disconnect from Sentry API + Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow + Disconnect-SentryApi + + Write-Host "Integration tests complete" -ForegroundColor Green +} diff --git a/integration-test/README.md b/integration-test/README.md index 05e8f346b..4918a4ffb 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -106,37 +106,38 @@ cd integration-test pwsh -Command "Invoke-Pester Integration.Tests.ps1" ``` -### Android (Local via adb) +### Android (Local via ADB) -```bash +```powershell # Ensure device/emulator is connected adb devices # Set environment variables -export SENTRY_UNREAL_TEST_DSN="https://key@org.ingest.sentry.io/project" -export SENTRY_AUTH_TOKEN="sntrys_your_token_here" -export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" +$env:SENTRY_UNREAL_TEST_DSN = "https://key@org.ingest.sentry.io/project" +$env:SENTRY_AUTH_TOKEN = "sntrys_your_token_here" +$env:SENTRY_UNREAL_TEST_APP_PATH = "./path/to/SentryPlayground.apk" -# Run tests +# Run tests (uses ADB by default) cd integration-test -pwsh -Command "Invoke-Pester ./Integration.Android.Adb.Tests.ps1" +Invoke-Pester Integration.Android.Tests.ps1 ``` ### Android (Cloud via SauceLabs) -```bash +```powershell # Set environment variables -export SENTRY_UNREAL_TEST_DSN="https://key@org.ingest.sentry.io/project" -export SENTRY_AUTH_TOKEN="sntrys_your_token_here" -export SENTRY_UNREAL_TEST_APP_PATH="./path/to/SentryPlayground.apk" -export SAUCE_USERNAME="your-saucelabs-username" -export SAUCE_ACCESS_KEY="your-saucelabs-access-key" -export SAUCE_REGION="us-west-1" -export SAUCE_DEVICE_NAME="Samsung_Galaxy_S23_15_real_sjc1" +$env:SENTRY_UNREAL_TEST_DSN = "https://key@org.ingest.sentry.io/project" +$env:SENTRY_AUTH_TOKEN = "sntrys_your_token_here" +$env:SENTRY_UNREAL_TEST_APP_PATH = "./path/to/SentryPlayground.apk" +$env:SAUCE_USERNAME = "your-saucelabs-username" +$env:SAUCE_ACCESS_KEY = "your-saucelabs-access-key" +$env:SAUCE_REGION = "us-west-1" +$env:SAUCE_DEVICE_NAME = "Samsung_Galaxy_S23_15_real_sjc1" -# Run tests +# Run tests with explicit platform selection cd integration-test -pwsh -Command "Invoke-Pester ./Integration.Android.SauceLabs.Tests.ps1" +$Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } +Invoke-Pester -Container $Container ``` **Note**: Ensure `SAUCE_DEVICE_NAME` matches a device available in your `SAUCE_REGION`. See the [SauceLabs Platform Configurator](https://app.saucelabs.com/live/web-testing) to find available devices for your region. From 49d01ee9611de603c6628c739dd414c2f9f5b050 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 13:26:57 +0200 Subject: [PATCH 43/52] Remove platform-specific test scripts --- .../Integration.Android.Adb.Tests.ps1 | 249 ---------------- .../Integration.Android.SauceLabs.Tests.ps1 | 273 ------------------ 2 files changed, 522 deletions(-) delete mode 100644 integration-test/Integration.Android.Adb.Tests.ps1 delete mode 100644 integration-test/Integration.Android.SauceLabs.Tests.ps1 diff --git a/integration-test/Integration.Android.Adb.Tests.ps1 b/integration-test/Integration.Android.Adb.Tests.ps1 deleted file mode 100644 index 74ad2e04c..000000000 --- a/integration-test/Integration.Android.Adb.Tests.ps1 +++ /dev/null @@ -1,249 +0,0 @@ -# Integration tests for Sentry Unreal SDK on Android via ADB -# Requires: -# - Pre-built APK (x64 for emulator, arm64 for device) -# - Android emulator or device connected -# - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -BeforeAll { - # Check if configuration file exists - $configFile = "$PSScriptRoot/TestConfig.local.ps1" - if (-not (Test-Path $configFile)) { - throw "Configuration file '$configFile' not found. Run 'cmake -B build -S .' first" - } - - # Load configuration (provides $global:AppRunnerPath) - . $configFile - - # Import app-runner modules (SentryApiClient, test utilities) - . "$global:AppRunnerPath/import-modules.ps1" - - # Validate environment variables - $script:DSN = $env:SENTRY_UNREAL_TEST_DSN - $script:AuthToken = $env:SENTRY_AUTH_TOKEN - $script:ApkPath = $env:SENTRY_UNREAL_TEST_APP_PATH - - if (-not $script:DSN) { - throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" - } - - if (-not $script:AuthToken) { - throw "Environment variable SENTRY_AUTH_TOKEN must be set" - } - - if (-not $script:ApkPath) { - throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" - } - - # Connect to Sentry API - Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow - Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken - - # Validate app path - if (-not (Test-Path $script:ApkPath)) { - throw "Application not found at: $script:ApkPath" - } - - # Create output directory - $script:OutputDir = "$PSScriptRoot/output" - if (-not (Test-Path $script:OutputDir)) { - New-Item -ItemType Directory -Path $script:OutputDir | Out-Null - } - - $script:PackageName = "io.sentry.unreal.sample" - $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - - # Connect to Android device via ADB (auto-discovers available device) - Write-Host "Connecting to Android device..." -ForegroundColor Yellow - Connect-Device -Platform AndroidAdb - - # Install APK to device - Write-Host "Installing APK to Android device..." -ForegroundColor Yellow - Install-DeviceApp -Path $script:ApkPath - - # ========================================== - # RUN 1: Crash test - creates minidump - # ========================================== - # The crash is captured but NOT uploaded yet (Android behavior). - # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) - - # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $cmdlineCrashArgs = "-e cmdline -crash-capture" - # $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs - - # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan - - # ========================================== - # RUN 2: Message test - uploads crash from Run 1 + captures message - # ========================================== - # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. - - Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - $cmdlineMessageArgs = "-e cmdline -message-capture" - $global:AndroidMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs - - Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan -} - -Describe "Sentry Unreal Android Integration Tests" { - - # ========================================== - # NOTE: Crash Capture Tests are DISABLED due to tag sync issue - # Uncomment when Android SDK tag persistence is fixed - # ========================================== - # Context "Crash Capture Tests" { - # BeforeAll { - # # Crash event is sent during the MESSAGE run (Run 2) - # # But the crash_id comes from the CRASH run (Run 1) - # $CrashResult = $global:AndroidCrashResult - # $CrashEvent = $null - # - # # Parse crash event ID from crash run output - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # - # if ($eventIds -and $eventIds.Count -gt 0) { - # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan - # $crashId = $eventIds[0] - # - # # Fetch crash event using the tag (event was sent during message run) - # try { - # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" - # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green - # } catch { - # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red - # } - # } else { - # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow - # } - # } - # - # It "Should output event ID before crash" { - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # $eventIds | Should -Not -BeNullOrEmpty - # $eventIds.Count | Should -Be 1 - # } - # - # It "Should capture crash event in Sentry (uploaded during next run)" { - # $CrashEvent | Should -Not -BeNullOrEmpty - # } - # - # It "Should have correct event type and platform" { - # $CrashEvent.type | Should -Be 'error' - # $CrashEvent.platform | Should -Be 'native' - # } - # - # It "Should have exception information" { - # $CrashEvent.exception | Should -Not -BeNullOrEmpty - # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty - # } - # - # It "Should have stack trace" { - # $exception = $CrashEvent.exception.values[0] - # $exception.stacktrace | Should -Not -BeNullOrEmpty - # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty - # } - # - # It "Should have user context" { - # $CrashEvent.user | Should -Not -BeNullOrEmpty - # $CrashEvent.user.username | Should -Be 'TestUser' - # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' - # $CrashEvent.user.id | Should -Be '12345' - # } - # - # It "Should have test.crash_id tag for correlation" { - # $tags = $CrashEvent.tags - # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } - # $crashIdTag | Should -Not -BeNullOrEmpty - # $crashIdTag.value | Should -Not -BeNullOrEmpty - # } - # - # It "Should have integration test tag" { - # $tags = $CrashEvent.tags - # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - # } - # - # It "Should have breadcrumbs from before crash" { - # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - # } - # } - - Context "Message Capture Tests" { - BeforeAll { - $MessageResult = $global:AndroidMessageResult - $MessageEvent = $null - - # Parse event ID from output - $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 - - if ($eventIds -and $eventIds.Count -gt 0) { - Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan - - # Fetch event from Sentry (with polling) - try { - $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] - Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green - } catch { - Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red - } - } else { - Write-Host "Warning: No message event ID found in output" -ForegroundColor Yellow - } - } - - It "Should output event ID" { - $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 - $eventIds | Should -Not -BeNullOrEmpty - $eventIds.Count | Should -Be 1 - } - - It "Should output TEST_RESULT with success" { - $testResultLine = $MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } - $testResultLine | Should -Not -BeNullOrEmpty - $testResultLine | Should -Match '"success"\s*:\s*true' - } - - It "Should capture message event in Sentry" { - $MessageEvent | Should -Not -BeNullOrEmpty - } - - It "Should have correct platform" { - # Android events are captured from Java layer, so platform is 'java' not 'native' - $MessageEvent.platform | Should -Be 'java' - } - - It "Should have message content" { - $MessageEvent.message | Should -Not -BeNullOrEmpty - $MessageEvent.message.formatted | Should -Match 'Integration test message' - } - - It "Should have user context" { - $MessageEvent.user | Should -Not -BeNullOrEmpty - $MessageEvent.user.username | Should -Be 'TestUser' - } - - It "Should have integration test tag" { - $tags = $MessageEvent.tags - ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - } - - It "Should have breadcrumbs" { - $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - } - } -} - -AfterAll { - # Disconnect from Android device - Write-Host "Disconnecting from Android device..." -ForegroundColor Yellow - Disconnect-Device - - # Disconnect from Sentry API - Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow - Disconnect-SentryApi - - Write-Host "Integration tests complete" -ForegroundColor Green -} \ No newline at end of file diff --git a/integration-test/Integration.Android.SauceLabs.Tests.ps1 b/integration-test/Integration.Android.SauceLabs.Tests.ps1 deleted file mode 100644 index b6b4d627e..000000000 --- a/integration-test/Integration.Android.SauceLabs.Tests.ps1 +++ /dev/null @@ -1,273 +0,0 @@ -# Integration tests for Sentry Unreal SDK on Android via SauceLabs Real Device Cloud -# Requires: -# - Pre-built APK -# - SauceLabs account credentials -# - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH -# SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME -# -# Note: SAUCE_DEVICE_NAME must match a device available in SAUCE_REGION. -# Example: For SAUCE_REGION=us-west-1, use devices with 'sjc1' suffix (San Jose DC1) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -BeforeAll { - # Check if configuration file exists - $configFile = "$PSScriptRoot/TestConfig.local.ps1" - if (-not (Test-Path $configFile)) { - throw "Configuration file '$configFile' not found. Run 'cmake -B build -S .' first" - } - - # Load configuration (provides $global:AppRunnerPath) - . $configFile - - # Import app-runner modules (SentryApiClient, test utilities) - . "$global:AppRunnerPath/import-modules.ps1" - - # Validate environment variables - $script:DSN = $env:SENTRY_UNREAL_TEST_DSN - $script:AuthToken = $env:SENTRY_AUTH_TOKEN - $script:ApkPath = $env:SENTRY_UNREAL_TEST_APP_PATH - $script:SauceUsername = $env:SAUCE_USERNAME - $script:SauceAccessKey = $env:SAUCE_ACCESS_KEY - $script:SauceRegion = $env:SAUCE_REGION - $script:SauceDeviceName = $env:SAUCE_DEVICE_NAME - - if (-not $script:DSN) { - throw "Environment variable SENTRY_UNREAL_TEST_DSN must be set" - } - - if (-not $script:AuthToken) { - throw "Environment variable SENTRY_AUTH_TOKEN must be set" - } - - if (-not $script:ApkPath) { - throw "Environment variable SENTRY_UNREAL_TEST_APP_PATH must be set" - } - - if (-not $script:SauceUsername) { - throw "Environment variable SAUCE_USERNAME must be set" - } - - if (-not $script:SauceAccessKey) { - throw "Environment variable SAUCE_ACCESS_KEY must be set" - } - - if (-not $script:SauceRegion) { - throw "Environment variable SAUCE_REGION must be set" - } - - if (-not $script:SauceDeviceName) { - throw "Environment variable SAUCE_DEVICE_NAME must be set" - } - - # Connect to Sentry API - Write-Host "Connecting to Sentry API..." -ForegroundColor Yellow - Connect-SentryApi -DSN $script:DSN -ApiToken $script:AuthToken - - # Validate app path - if (-not (Test-Path $script:ApkPath)) { - throw "Application not found at: $script:ApkPath" - } - - # Create output directory - $script:OutputDir = "$PSScriptRoot/output" - if (-not (Test-Path $script:OutputDir)) { - New-Item -ItemType Directory -Path $script:OutputDir | Out-Null - } - - $script:PackageName = "io.sentry.unreal.sample" - $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" - - # Connect to SauceLabs (reads all configuration from environment variables) - Write-Host "Connecting to SauceLabs..." -ForegroundColor Yellow - Connect-Device -Platform AndroidSauceLabs - - # Install APK to SauceLabs device - Write-Host "Installing APK to SauceLabs device..." -ForegroundColor Yellow - Install-DeviceApp -Path $script:ApkPath - - # ========================================== - # RUN 1: Crash test - creates minidump - # ========================================== - # The crash is captured but NOT uploaded yet (Android behavior). - # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) - - # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $cmdlineCrashArgs = "-e cmdline -crash-capture" - # $global:SauceLabsCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs - - # Write-Host "Crash test exit code: $($global:SauceLabsCrashResult.ExitCode)" -ForegroundColor Cyan - - # ========================================== - # RUN 2: Message test - uploads crash from Run 1 + captures message - # ========================================== - # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. - - Write-Host "Running message-capture test (will upload crash from previous run)..." -ForegroundColor Yellow - $cmdlineMessageArgs = "-e cmdline -message-capture" - $global:SauceLabsMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs - - Write-Host "Message test exit code: $($global:SauceLabsMessageResult.ExitCode)" -ForegroundColor Cyan -} - -Describe "Sentry Unreal Android Integration Tests (SauceLabs)" { - - # ========================================== - # NOTE: Crash Capture Tests are DISABLED due to tag sync issue - # Uncomment when Android SDK tag persistence is fixed - # ========================================== - # Context "Crash Capture Tests" { - # BeforeAll { - # # Crash event is sent during the MESSAGE run (Run 2) - # # But the crash_id comes from the CRASH run (Run 1) - # $CrashResult = $global:SauceLabsCrashResult - # $CrashEvent = $null - # - # # Parse crash event ID from crash run output - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # - # if ($eventIds -and $eventIds.Count -gt 0) { - # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan - # $crashId = $eventIds[0] - # - # # Fetch crash event using the tag (event was sent during message run) - # try { - # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" - # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green - # } catch { - # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red - # } - # } else { - # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow - # } - # } - # - # It "Should output event ID before crash" { - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # $eventIds | Should -Not -BeNullOrEmpty - # $eventIds.Count | Should -Be 1 - # } - # - # It "Should capture crash event in Sentry (uploaded during next run)" { - # $CrashEvent | Should -Not -BeNullOrEmpty - # } - # - # It "Should have correct event type and platform" { - # $CrashEvent.type | Should -Be 'error' - # $CrashEvent.platform | Should -Be 'native' - # } - # - # It "Should have exception information" { - # $CrashEvent.exception | Should -Not -BeNullOrEmpty - # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty - # } - # - # It "Should have stack trace" { - # $exception = $CrashEvent.exception.values[0] - # $exception.stacktrace | Should -Not -BeNullOrEmpty - # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty - # } - # - # It "Should have user context" { - # $CrashEvent.user | Should -Not -BeNullOrEmpty - # $CrashEvent.user.username | Should -Be 'TestUser' - # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' - # $CrashEvent.user.id | Should -Be '12345' - # } - # - # It "Should have test.crash_id tag for correlation" { - # $tags = $CrashEvent.tags - # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } - # $crashIdTag | Should -Not -BeNullOrEmpty - # $crashIdTag.value | Should -Not -BeNullOrEmpty - # } - # - # It "Should have integration test tag" { - # $tags = $CrashEvent.tags - # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - # } - # - # It "Should have breadcrumbs from before crash" { - # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - # } - # } - - Context "Message Capture Tests" { - BeforeAll { - $MessageResult = $global:SauceLabsMessageResult - $MessageEvent = $null - - # Parse event ID from output - $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 - - if ($eventIds -and $eventIds.Count -gt 0) { - Write-Host "Message event ID captured: $($eventIds[0])" -ForegroundColor Cyan - - # Fetch event from Sentry (with polling) - try { - $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] - Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green - } catch { - Write-Host "Failed to fetch message event from Sentry: $_" -ForegroundColor Red - } - } else { - Write-Host "Warning: No message event ID found in output" -ForegroundColor Yellow - } - } - - It "Should output event ID" { - $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 - $eventIds | Should -Not -BeNullOrEmpty - $eventIds.Count | Should -Be 1 - } - - It "Should output TEST_RESULT with success" { - $testResultLine = $MessageResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } - $testResultLine | Should -Not -BeNullOrEmpty - $testResultLine | Should -Match '"success"\s*:\s*true' - } - - It "Should capture message event in Sentry" { - $MessageEvent | Should -Not -BeNullOrEmpty - } - - It "Should have correct platform" { - # Android events are captured from Java layer, so platform is 'java' not 'native' - $MessageEvent.platform | Should -Be 'java' - } - - It "Should have message content" { - $MessageEvent.message | Should -Not -BeNullOrEmpty - $MessageEvent.message.formatted | Should -Match 'Integration test message' - } - - It "Should have user context" { - $MessageEvent.user | Should -Not -BeNullOrEmpty - $MessageEvent.user.username | Should -Be 'TestUser' - } - - It "Should have integration test tag" { - $tags = $MessageEvent.tags - ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - } - - It "Should have breadcrumbs" { - $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - } - } -} - -AfterAll { - # Disconnect from SauceLabs device (cleans up session) - Write-Host "Disconnecting from SauceLabs device..." -ForegroundColor Yellow - Disconnect-Device - - # Disconnect from Sentry API - Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow - Disconnect-SentryApi - - Write-Host "Integration tests complete" -ForegroundColor Green -} From e4d0fb4f1de8ab8c1c5146ca435378384dba33aa Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 13:43:53 +0200 Subject: [PATCH 44/52] Add SauceLabs session name override --- .github/workflows/integration-test-android.yml | 2 +- integration-test/CMakeLists.txt | 2 +- integration-test/README.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index f83a831e5..81e4d1334 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -16,6 +16,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SAUCE_REGION: us-west-1 SAUCE_DEVICE_NAME: Samsung_Galaxy_S23_15_real_sjc1 + SAUCE_SESSION_NAME: UE ${{ inputs.unreal-version }} Android Integration Test steps: - uses: actions/checkout@v4 @@ -36,7 +37,6 @@ jobs: SENTRY_UNREAL_TEST_DSN: ${{ secrets.SENTRY_UNREAL_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-arm64.apk - UNREAL_VERSION: ${{ inputs.unreal-version }} run: | cmake -B build -S . $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } diff --git a/integration-test/CMakeLists.txt b/integration-test/CMakeLists.txt index 0ede738bc..3756fa532 100644 --- a/integration-test/CMakeLists.txt +++ b/integration-test/CMakeLists.txt @@ -7,7 +7,7 @@ include(FetchContent) FetchContent_Declare( app-runner GIT_REPOSITORY https://github.com/getsentry/app-runner.git - GIT_TAG 4a55e0a77c85b88cc3fd8b97724b473728d8aeaa + GIT_TAG bd38cdc7be2e001e21394abe1406050f99f690ce ) FetchContent_MakeAvailable(app-runner) diff --git a/integration-test/README.md b/integration-test/README.md index 4918a4ffb..12eb05884 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -33,6 +33,7 @@ Supports testing on: - `SAUCE_ACCESS_KEY` - SauceLabs access key - `SAUCE_REGION` - SauceLabs region (e.g., `us-west-1`, `eu-central-1`) - `SAUCE_DEVICE_NAME` - Device name available in the specified region (must match region datacenter suffix) + - `SAUCE_SESSION_NAME` - Session name for SauceLabs dashboard (optional, defaults to "App Runner Android Test") **Note**: The device name must match a device available in your SauceLabs region. Device names include a datacenter suffix that must align with the region: - `us-west-1` → devices ending in `_sjc1` (San Jose DC1) @@ -133,6 +134,7 @@ $env:SAUCE_USERNAME = "your-saucelabs-username" $env:SAUCE_ACCESS_KEY = "your-saucelabs-access-key" $env:SAUCE_REGION = "us-west-1" $env:SAUCE_DEVICE_NAME = "Samsung_Galaxy_S23_15_real_sjc1" +$env:SAUCE_SESSION_NAME = "My Custom Test Session" # Optional, defaults to "App Runner Android Test" # Run tests with explicit platform selection cd integration-test From 95d2fe6da0a371729a77bf6c829531ed5d8e6379 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 26 Nov 2025 15:00:04 +0200 Subject: [PATCH 45/52] Bump app-runner --- integration-test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/CMakeLists.txt b/integration-test/CMakeLists.txt index 3756fa532..c9436c232 100644 --- a/integration-test/CMakeLists.txt +++ b/integration-test/CMakeLists.txt @@ -7,7 +7,7 @@ include(FetchContent) FetchContent_Declare( app-runner GIT_REPOSITORY https://github.com/getsentry/app-runner.git - GIT_TAG bd38cdc7be2e001e21394abe1406050f99f690ce + GIT_TAG da7d0da08618dc74d2420f47b9e3a4dc6f69518e ) FetchContent_MakeAvailable(app-runner) From a6fda8b81d6d912b1c060b3d9c738c6693401b11 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 28 Nov 2025 09:34:13 +0200 Subject: [PATCH 46/52] Fix Android crash capturing test --- .../Integration.Android.Tests.ps1 | 160 +++++++++--------- .../Source/Sentry/Sentry_Android_UPL.xml | 1 + .../SentryPlaygroundGameInstance.cpp | 2 + 3 files changed, 83 insertions(+), 80 deletions(-) diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 index 6d450dc7c..6548ad488 100644 --- a/integration-test/Integration.Android.Tests.ps1 +++ b/integration-test/Integration.Android.Tests.ps1 @@ -98,11 +98,11 @@ BeforeAll { # The crash is captured but NOT uploaded yet (Android behavior). # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) - # Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - # $cmdlineCrashArgs = "-e cmdline -crash-capture" - # $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs + Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow + $cmdlineCrashArgs = "-e cmdline -crash-capture" + $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs - # Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan # ========================================== # RUN 2: Message test - uploads crash from Run 1 + captures message @@ -122,82 +122,82 @@ Describe "Sentry Unreal Android Integration Tests ($Platform)" { # NOTE: Crash Capture Tests are DISABLED due to tag sync issue # Uncomment when Android SDK tag persistence is fixed # ========================================== - # Context "Crash Capture Tests" { - # BeforeAll { - # # Crash event is sent during the MESSAGE run (Run 2) - # # But the crash_id comes from the CRASH run (Run 1) - # $CrashResult = $global:AndroidCrashResult - # $CrashEvent = $null - # - # # Parse crash event ID from crash run output - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # - # if ($eventIds -and $eventIds.Count -gt 0) { - # Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan - # $crashId = $eventIds[0] - # - # # Fetch crash event using the tag (event was sent during message run) - # try { - # $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" - # Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green - # } catch { - # Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red - # } - # } else { - # Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow - # } - # } - # - # It "Should output event ID before crash" { - # $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 - # $eventIds | Should -Not -BeNullOrEmpty - # $eventIds.Count | Should -Be 1 - # } - # - # It "Should capture crash event in Sentry (uploaded during next run)" { - # $CrashEvent | Should -Not -BeNullOrEmpty - # } - # - # It "Should have correct event type and platform" { - # $CrashEvent.type | Should -Be 'error' - # $CrashEvent.platform | Should -Be 'native' - # } - # - # It "Should have exception information" { - # $CrashEvent.exception | Should -Not -BeNullOrEmpty - # $CrashEvent.exception.values | Should -Not -BeNullOrEmpty - # } - # - # It "Should have stack trace" { - # $exception = $CrashEvent.exception.values[0] - # $exception.stacktrace | Should -Not -BeNullOrEmpty - # $exception.stacktrace.frames | Should -Not -BeNullOrEmpty - # } - # - # It "Should have user context" { - # $CrashEvent.user | Should -Not -BeNullOrEmpty - # $CrashEvent.user.username | Should -Be 'TestUser' - # $CrashEvent.user.email | Should -Be 'user-mail@test.abc' - # $CrashEvent.user.id | Should -Be '12345' - # } - # - # It "Should have test.crash_id tag for correlation" { - # $tags = $CrashEvent.tags - # $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } - # $crashIdTag | Should -Not -BeNullOrEmpty - # $crashIdTag.value | Should -Not -BeNullOrEmpty - # } - # - # It "Should have integration test tag" { - # $tags = $CrashEvent.tags - # ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' - # } - # - # It "Should have breadcrumbs from before crash" { - # $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - # $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty - # } - # } + Context "Crash Capture Tests" { + BeforeAll { + # Crash event is sent during the MESSAGE run (Run 2) + # But the crash_id comes from the CRASH run (Run 1) + $CrashResult = $global:AndroidCrashResult + $CrashEvent = $null + + # Parse crash event ID from crash run output + $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + + if ($eventIds -and $eventIds.Count -gt 0) { + Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan + $crashId = $eventIds[0] + + # Fetch crash event using the tag (event was sent during message run) + try { + $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green + } catch { + Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red + } + } else { + Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow + } + } + + It "Should output event ID before crash" { + $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + $eventIds | Should -Not -BeNullOrEmpty + $eventIds.Count | Should -Be 1 + } + + It "Should capture crash event in Sentry (uploaded during next run)" { + $CrashEvent | Should -Not -BeNullOrEmpty + } + + It "Should have correct event type and platform" { + $CrashEvent.type | Should -Be 'error' + $CrashEvent.platform | Should -Be 'native' + } + + It "Should have exception information" { + $CrashEvent.exception | Should -Not -BeNullOrEmpty + $CrashEvent.exception.values | Should -Not -BeNullOrEmpty + } + + It "Should have stack trace" { + $exception = $CrashEvent.exception.values[0] + $exception.stacktrace | Should -Not -BeNullOrEmpty + $exception.stacktrace.frames | Should -Not -BeNullOrEmpty + } + + It "Should have user context" { + $CrashEvent.user | Should -Not -BeNullOrEmpty + $CrashEvent.user.username | Should -Be 'TestUser' + $CrashEvent.user.email | Should -Be 'user-mail@test.abc' + $CrashEvent.user.id | Should -Be '12345' + } + + It "Should have test.crash_id tag for correlation" { + $tags = $CrashEvent.tags + $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } + $crashIdTag | Should -Not -BeNullOrEmpty + $crashIdTag.value | Should -Not -BeNullOrEmpty + } + + It "Should have integration test tag" { + $tags = $CrashEvent.tags + ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' + } + + It "Should have breadcrumbs from before crash" { + $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } Context "Message Capture Tests" { BeforeAll { diff --git a/plugin-dev/Source/Sentry/Sentry_Android_UPL.xml b/plugin-dev/Source/Sentry/Sentry_Android_UPL.xml index a3d04141c..95374b0bd 100644 --- a/plugin-dev/Source/Sentry/Sentry_Android_UPL.xml +++ b/plugin-dev/Source/Sentry/Sentry_Android_UPL.xml @@ -63,6 +63,7 @@ + diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index 9a525c350..d28b8066a 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -92,6 +92,8 @@ void USentryPlaygroundGameInstance::RunCrashTest() SentrySubsystem->SetTag(TEXT("test.crash_id"), EventId); + FPlatformProcess::Sleep(1.0f); + USentryPlaygroundUtils::Terminate(ESentryAppTerminationType::NullPointer); } From 1451ceda712f9970537a0eb8183ee7af88d7adf3 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 28 Nov 2025 10:15:12 +0200 Subject: [PATCH 47/52] Fix newline at end of integration-test-android.yml --- .github/workflows/integration-test-android.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index 81e4d1334..e1f25cc1a 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -49,4 +49,5 @@ jobs: name: UE ${{ inputs.unreal-version }} integration test output (Android) path: | integration-test/output/ - retention-days: 14 \ No newline at end of file + retention-days: 14 + From 520b3e355d520ea83b052c8324fd3fc7ce73f4f8 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 28 Nov 2025 10:23:45 +0200 Subject: [PATCH 48/52] Clean up comments related to crash capture tests Removed TODO and notes regarding crash capture tests due to Android SDK tag persistence issues. --- integration-test/Integration.Android.Tests.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 index 6548ad488..de2f673ac 100644 --- a/integration-test/Integration.Android.Tests.ps1 +++ b/integration-test/Integration.Android.Tests.ps1 @@ -96,7 +96,6 @@ BeforeAll { # RUN 1: Crash test - creates minidump # ========================================== # The crash is captured but NOT uploaded yet (Android behavior). - # TODO: Re-enable once Android SDK tag persistence is fixed (`test.crash_id` tag set before crash is not synced to the captured crash on Android) Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow $cmdlineCrashArgs = "-e cmdline -crash-capture" @@ -118,10 +117,6 @@ BeforeAll { Describe "Sentry Unreal Android Integration Tests ($Platform)" { - # ========================================== - # NOTE: Crash Capture Tests are DISABLED due to tag sync issue - # Uncomment when Android SDK tag persistence is fixed - # ========================================== Context "Crash Capture Tests" { BeforeAll { # Crash event is sent during the MESSAGE run (Run 2) From 8c75f1d2d5a3b63336c878b441d3876dffce174c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 3 Dec 2025 09:06:35 +0200 Subject: [PATCH 49/52] Update app-runner GIT_TAG to latest version --- integration-test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/CMakeLists.txt b/integration-test/CMakeLists.txt index c9436c232..989d73604 100644 --- a/integration-test/CMakeLists.txt +++ b/integration-test/CMakeLists.txt @@ -7,7 +7,7 @@ include(FetchContent) FetchContent_Declare( app-runner GIT_REPOSITORY https://github.com/getsentry/app-runner.git - GIT_TAG da7d0da08618dc74d2420f47b9e3a4dc6f69518e + GIT_TAG 2f5268c4c27d435417cdf1dadc8980126b9bd64f ) FetchContent_MakeAvailable(app-runner) From 046f867549007bdedd7b4361d6ae26e3e57422fd Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Wed, 3 Dec 2025 16:30:23 +0200 Subject: [PATCH 50/52] Update Android test script --- .../Integration.Android.Tests.ps1 | 145 ++++++++++++------ integration-test/README.md | 24 +-- 2 files changed, 102 insertions(+), 67 deletions(-) diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 index de2f673ac..60f81059c 100644 --- a/integration-test/Integration.Android.Tests.ps1 +++ b/integration-test/Integration.Android.Tests.ps1 @@ -2,16 +2,8 @@ # Supports both ADB (local devices/emulators) and SauceLabs (cloud devices) # # Usage: -# # Default (uses ADB) # Invoke-Pester Integration.Android.Tests.ps1 # -# # Explicit platform selection with Pester containers -# $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'Adb' } -# Invoke-Pester -Container $Container -# -# $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } -# Invoke-Pester -Container $Container -# # Requires: # - Pre-built APK # - Environment variables: SENTRY_UNREAL_TEST_DSN, SENTRY_AUTH_TOKEN, SENTRY_UNREAL_TEST_APP_PATH @@ -20,23 +12,70 @@ # - Android emulator or device connected via ADB # # For SauceLabs: -# - SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME - -param( - [Parameter(Mandatory = $false)] - [ValidateSet('Adb', 'SauceLabs')] - [string]$Platform = 'Adb' -) +# - SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME, SAUCE_SESSION_NAME Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -BeforeAll { - # Map friendly platform name to app-runner platform name - $appRunnerPlatform = if ($Platform -eq 'Adb') { 'AndroidAdb' } else { 'AndroidSauceLabs' } +BeforeDiscovery { + # Define test targets + function Get-TestTarget { + param( + [string]$Platform, + [string]$ProviderName + ) + + return @{ + Platform = $Platform + ProviderName = $ProviderName + } + } + + $TestTargets = @() + + # Detect if running in CI environment + # In CI, running tests using SauceLabs is mandatory while adb tests are skipped due to emulator limitations + $isCI = $env:CI -eq 'true' + + # Check adb test configuration + if (Get-Command 'adb' -ErrorAction SilentlyContinue) { + # Check if any devices are connected + $adbDevices = adb devices + if ($adbDevices -match '\tdevice$') { + $TestTargets += Get-TestTarget -Platform 'Adb' -ProviderName 'Adb' + } + else { + Write-Host "No devices connected via adb. Adb tests will be skipped." + } + } + else { + Write-Host "adb not found in PATH. Adb tests will be skipped." + } + + # Check SauceLabs test configuration + if ($env:SAUCE_USERNAME -and $env:SAUCE_ACCESS_KEY -and $env:SAUCE_REGION -and $env:SAUCE_DEVICE_NAME -and $env:SAUCE_SESSION_NAME) { + $TestTargets += Get-TestTarget -Platform 'SauceLabs' -ProviderName 'AndroidSauceLabs' + } + else { + $message = "SauceLabs credentials not found" + if ($isCI) { + throw "$message. These are required in CI." + } + else { + Write-Host "$message. SauceLabs tests will be skipped." + } + } - Write-Host "Running Android tests with platform: $Platform (app-runner: $appRunnerPlatform)" -ForegroundColor Cyan + # Inform user if no test targets are available + if ($TestTargets.Count -eq 0) { + Write-Warning "No Android test targets detected. Integration tests will be skipped." + Write-Warning "To run Android integration tests, configure at least one test target:" + Write-Warning " - Adb: ADB must be in PATH and at least one Android device must be connected (physical or emulator)" + Write-Warning " - SauceLabs: Environment variables SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION, SAUCE_DEVICE_NAME, SAUCE_SESSION_NAME must be set" + } +} +BeforeAll { # Check if configuration file exists $configFile = "$PSScriptRoot/TestConfig.local.ps1" if (-not (Test-Path $configFile)) { @@ -83,39 +122,49 @@ BeforeAll { $script:PackageName = "io.sentry.unreal.sample" $script:ActivityName = "$script:PackageName/com.epicgames.unreal.GameActivity" +} - # Connect to Android device (provider validates its own env vars) - Write-Host "Connecting to Android via $Platform..." -ForegroundColor Yellow - Connect-Device -Platform $appRunnerPlatform +Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTargets { - # Install APK - Write-Host "Installing APK via $Platform..." -ForegroundColor Yellow - Install-DeviceApp -Path $script:ApkPath + BeforeAll { + # Connect to Android device (provider validates its own env vars) + Write-Host "Connecting to Android via $Platform..." -ForegroundColor Yellow + Connect-Device -Platform $ProviderName - # ========================================== - # RUN 1: Crash test - creates minidump - # ========================================== - # The crash is captured but NOT uploaded yet (Android behavior). + # Install APK + Write-Host "Installing APK via $Platform..." -ForegroundColor Yellow + Install-DeviceApp -Path $script:ApkPath - Write-Host "Running crash-capture test (will crash)..." -ForegroundColor Yellow - $cmdlineCrashArgs = "-e cmdline -crash-capture" - $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs + # ========================================== + # RUN 1: Crash test - creates minidump + # ========================================== + # The crash is captured but NOT uploaded yet (Android behavior). - Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan + Write-Host "Running crash-capture test (will crash) on $Platform..." -ForegroundColor Yellow + $cmdlineCrashArgs = "-e cmdline -crash-capture" + $global:AndroidCrashResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineCrashArgs - # ========================================== - # RUN 2: Message test - uploads crash from Run 1 + captures message - # ========================================== - # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. + Write-Host "Crash test exit code: $($global:AndroidCrashResult.ExitCode)" -ForegroundColor Cyan - Write-Host "Running message-capture test on $Platform..." -ForegroundColor Yellow - $cmdlineMessageArgs = "-e cmdline -message-capture" - $global:AndroidMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs + # ========================================== + # RUN 2: Message test - uploads crash from Run 1 + captures message + # ========================================== + # Currently we need to run again so that Sentry sends the crash event captured during the previous app session. - Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan -} + Write-Host "Running message-capture test on $Platform..." -ForegroundColor Yellow + $cmdlineMessageArgs = "-e cmdline -message-capture" + $global:AndroidMessageResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $cmdlineMessageArgs + + Write-Host "Message test exit code: $($global:AndroidMessageResult.ExitCode)" -ForegroundColor Cyan + } + + AfterAll { + # Disconnect from Android device + Write-Host "Disconnecting from $Platform..." -ForegroundColor Yellow + Disconnect-Device -Describe "Sentry Unreal Android Integration Tests ($Platform)" { + Write-Host "Integration tests complete on $Platform" -ForegroundColor Green + } Context "Crash Capture Tests" { BeforeAll { @@ -135,10 +184,12 @@ Describe "Sentry Unreal Android Integration Tests ($Platform)" { try { $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green - } catch { + } + catch { Write-Host "Failed to fetch crash event from Sentry: $_" -ForegroundColor Red } - } else { + } + else { Write-Host "Warning: No crash event ID found in output" -ForegroundColor Yellow } } @@ -263,10 +314,6 @@ Describe "Sentry Unreal Android Integration Tests ($Platform)" { } AfterAll { - # Disconnect from Android device - Write-Host "Disconnecting from $Platform..." -ForegroundColor Yellow - Disconnect-Device - # Disconnect from Sentry API Write-Host "Disconnecting from Sentry API..." -ForegroundColor Yellow Disconnect-SentryApi diff --git a/integration-test/README.md b/integration-test/README.md index 12eb05884..1d51bb9bf 100644 --- a/integration-test/README.md +++ b/integration-test/README.md @@ -107,39 +107,27 @@ cd integration-test pwsh -Command "Invoke-Pester Integration.Tests.ps1" ``` -### Android (Local via ADB) +### Android ```powershell -# Ensure device/emulator is connected -adb devices - # Set environment variables $env:SENTRY_UNREAL_TEST_DSN = "https://key@org.ingest.sentry.io/project" $env:SENTRY_AUTH_TOKEN = "sntrys_your_token_here" $env:SENTRY_UNREAL_TEST_APP_PATH = "./path/to/SentryPlayground.apk" -# Run tests (uses ADB by default) -cd integration-test -Invoke-Pester Integration.Android.Tests.ps1 -``` - -### Android (Cloud via SauceLabs) +# Ensure device/emulator is connected (for ADB) +adb devices -```powershell -# Set environment variables -$env:SENTRY_UNREAL_TEST_DSN = "https://key@org.ingest.sentry.io/project" -$env:SENTRY_AUTH_TOKEN = "sntrys_your_token_here" -$env:SENTRY_UNREAL_TEST_APP_PATH = "./path/to/SentryPlayground.apk" +# Set credentials (for SauceLabs) $env:SAUCE_USERNAME = "your-saucelabs-username" $env:SAUCE_ACCESS_KEY = "your-saucelabs-access-key" $env:SAUCE_REGION = "us-west-1" $env:SAUCE_DEVICE_NAME = "Samsung_Galaxy_S23_15_real_sjc1" $env:SAUCE_SESSION_NAME = "My Custom Test Session" # Optional, defaults to "App Runner Android Test" -# Run tests with explicit platform selection +# Run tests (uses ADB by default) cd integration-test -$Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } -Invoke-Pester -Container $Container +Invoke-Pester Integration.Android.Tests.ps1 ``` **Note**: Ensure `SAUCE_DEVICE_NAME` matches a device available in your `SAUCE_REGION`. See the [SauceLabs Platform Configurator](https://app.saucelabs.com/live/web-testing) to find available devices for your region. From a32ab7125109db04566ebec93aa54c6351580fae Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 4 Dec 2025 08:58:43 +0200 Subject: [PATCH 51/52] Update tests invocation --- .github/workflows/integration-test-android.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-test-android.yml b/.github/workflows/integration-test-android.yml index e1f25cc1a..aff604fb1 100644 --- a/.github/workflows/integration-test-android.yml +++ b/.github/workflows/integration-test-android.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest env: + GITHUB_TOKEN: ${{ github.token }} SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - GITHUB_TOKEN: ${{ github.token }} SAUCE_REGION: us-west-1 SAUCE_DEVICE_NAME: Samsung_Galaxy_S23_15_real_sjc1 SAUCE_SESSION_NAME: UE ${{ inputs.unreal-version }} Android Integration Test @@ -39,8 +39,7 @@ jobs: SENTRY_UNREAL_TEST_APP_PATH: ${{ github.workspace }}/sample-build/SentryPlayground-arm64.apk run: | cmake -B build -S . - $Container = New-PesterContainer -Path 'Integration.Android.Tests.ps1' -Data @{ Platform = 'SauceLabs' } - Invoke-Pester -Container $Container -CI + Invoke-Pester Integration.Android.Tests.ps1 -CI - name: Upload integration test output if: ${{ always() && steps.run-integration-tests.outcome == 'failure' }} From 622670840ed7732033ba3a9af509a831bf2476e0 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 4 Dec 2025 09:49:12 +0200 Subject: [PATCH 52/52] Fix variable declarations in test script --- .github/workflows/ci.yml | 2 +- .../Integration.Android.Tests.ps1 | 62 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd9ab44db..8176ea72c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,7 +205,7 @@ jobs: secrets: inherit strategy: fail-fast: false - max-parallel: 2 + max-parallel: 1 matrix: unreal: ['5.4', '5.5', '5.6', '5.7'] uses: ./.github/workflows/integration-test-android.yml diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 index 60f81059c..faaa1bbdd 100644 --- a/integration-test/Integration.Android.Tests.ps1 +++ b/integration-test/Integration.Android.Tests.ps1 @@ -170,11 +170,11 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa BeforeAll { # Crash event is sent during the MESSAGE run (Run 2) # But the crash_id comes from the CRASH run (Run 1) - $CrashResult = $global:AndroidCrashResult - $CrashEvent = $null + $script:CrashResult = $global:AndroidCrashResult + $script:CrashEvent = $null # Parse crash event ID from crash run output - $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 if ($eventIds -and $eventIds.Count -gt 0) { Write-Host "Crash ID captured: $($eventIds[0])" -ForegroundColor Cyan @@ -182,7 +182,7 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa # Fetch crash event using the tag (event was sent during message run) try { - $CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" + $script:CrashEvent = Get-SentryTestEvent -TagName 'test.crash_id' -TagValue "$crashId" Write-Host "Crash event fetched from Sentry successfully" -ForegroundColor Green } catch { @@ -195,60 +195,60 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa } It "Should output event ID before crash" { - $eventIds = Get-EventIds -AppOutput $CrashResult.Output -ExpectedCount 1 + $eventIds = Get-EventIds -AppOutput $script:CrashResult.Output -ExpectedCount 1 $eventIds | Should -Not -BeNullOrEmpty $eventIds.Count | Should -Be 1 } It "Should capture crash event in Sentry (uploaded during next run)" { - $CrashEvent | Should -Not -BeNullOrEmpty + $script:CrashEvent | Should -Not -BeNullOrEmpty } It "Should have correct event type and platform" { - $CrashEvent.type | Should -Be 'error' - $CrashEvent.platform | Should -Be 'native' + $script:CrashEvent.type | Should -Be 'error' + $script:CrashEvent.platform | Should -Be 'native' } It "Should have exception information" { - $CrashEvent.exception | Should -Not -BeNullOrEmpty - $CrashEvent.exception.values | Should -Not -BeNullOrEmpty + $script:CrashEvent.exception | Should -Not -BeNullOrEmpty + $script:CrashEvent.exception.values | Should -Not -BeNullOrEmpty } It "Should have stack trace" { - $exception = $CrashEvent.exception.values[0] + $exception = $script:CrashEvent.exception.values[0] $exception.stacktrace | Should -Not -BeNullOrEmpty $exception.stacktrace.frames | Should -Not -BeNullOrEmpty } It "Should have user context" { - $CrashEvent.user | Should -Not -BeNullOrEmpty - $CrashEvent.user.username | Should -Be 'TestUser' - $CrashEvent.user.email | Should -Be 'user-mail@test.abc' - $CrashEvent.user.id | Should -Be '12345' + $script:CrashEvent.user | Should -Not -BeNullOrEmpty + $script:CrashEvent.user.username | Should -Be 'TestUser' + $script:CrashEvent.user.email | Should -Be 'user-mail@test.abc' + $script:CrashEvent.user.id | Should -Be '12345' } It "Should have test.crash_id tag for correlation" { - $tags = $CrashEvent.tags + $tags = $script:CrashEvent.tags $crashIdTag = $tags | Where-Object { $_.key -eq 'test.crash_id' } $crashIdTag | Should -Not -BeNullOrEmpty $crashIdTag.value | Should -Not -BeNullOrEmpty } It "Should have integration test tag" { - $tags = $CrashEvent.tags + $tags = $script:CrashEvent.tags ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' } It "Should have breadcrumbs from before crash" { - $CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + $script:CrashEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $script:CrashEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty } } Context "Message Capture Tests" { BeforeAll { - $MessageResult = $global:AndroidMessageResult - $MessageEvent = $null + $script:MessageResult = $global:AndroidMessageResult + $script:MessageEvent = $null # Parse event ID from output $eventIds = Get-EventIds -AppOutput $MessageResult.Output -ExpectedCount 1 @@ -258,7 +258,7 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa # Fetch event from Sentry (with polling) try { - $MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] + $script:MessageEvent = Get-SentryTestEvent -EventId $eventIds[0] Write-Host "Message event fetched from Sentry successfully" -ForegroundColor Green } catch { @@ -283,32 +283,32 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa } It "Should capture message event in Sentry" { - $MessageEvent | Should -Not -BeNullOrEmpty + $script:MessageEvent | Should -Not -BeNullOrEmpty } It "Should have correct platform" { # Android events are captured from Java layer, so platform is 'java' not 'native' - $MessageEvent.platform | Should -Be 'java' + $script:MessageEvent.platform | Should -Be 'java' } It "Should have message content" { - $MessageEvent.message | Should -Not -BeNullOrEmpty - $MessageEvent.message.formatted | Should -Match 'Integration test message' + $script:MessageEvent.message | Should -Not -BeNullOrEmpty + $script:MessageEvent.message.formatted | Should -Match 'Integration test message' } It "Should have user context" { - $MessageEvent.user | Should -Not -BeNullOrEmpty - $MessageEvent.user.username | Should -Be 'TestUser' + $script:MessageEvent.user | Should -Not -BeNullOrEmpty + $script:MessageEvent.user.username | Should -Be 'TestUser' } It "Should have integration test tag" { - $tags = $MessageEvent.tags + $tags = $script:MessageEvent.tags ($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration' } It "Should have breadcrumbs" { - $MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty - $MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + $script:MessageEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $script:MessageEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty } } }