diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d0eb74e5..cc59be993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,9 +258,10 @@ jobs: unity-version: ${{ matrix.unity-version }} smoke-test-run-android: - name: Run Android ${{ matrix.unity-version }} Smoke Test + name: Run Android ${{ matrix.unity-version }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} needs: [smoke-test-build-android, create-unity-matrix] + secrets: inherit uses: ./.github/workflows/smoke-test-run-android.yml with: unity-version: ${{ matrix.unity-version }} @@ -270,8 +271,10 @@ jobs: fail-fast: false matrix: unity-version: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version }} - api-level: [30, 31, 34] - init-type: ["runtime", "buildtime"] + # api-level: [30, 31, 34] + api-level: [30] + # init-type: ["runtime", "buildtime"] + init-type: ["runtime"] smoke-test-build-ios: name: Build iOS ${{ matrix.unity-version }} Smoke Test diff --git a/.github/workflows/smoke-test-build-android.yml b/.github/workflows/smoke-test-build-android.yml index a2614f06e..32b7ac7e3 100644 --- a/.github/workflows/smoke-test-build-android.yml +++ b/.github/workflows/smoke-test-build-android.yml @@ -77,7 +77,9 @@ jobs: run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - name: Configure Sentry - run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -CheckSymbols + run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -CheckSymbols -TestMode "integration" + env: + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Export APK - Runtime Initialization run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -CheckSymbols:$true -UnityVersion "$env:UNITY_VERSION" @@ -99,23 +101,24 @@ jobs: path: samples/IntegrationTest/Build/*.apk # Collect app but ignore the files that are not required for the test. retention-days: 14 # Lower retention period - we only need this to retry CI. - - name: Overwrite OptionsConfiguration for build-time initialization - run: | - $optionsPath = "samples/IntegrationTest/Assets/Scripts/OptionsConfiguration.cs" - $content = Get-Content $optionsPath -Raw - $content = $content -replace 'AndroidNativeInitializationType = NativeInitializationType.Runtime', 'AndroidNativeInitializationType = NativeInitializationType.BuildTime' - Set-Content $optionsPath $content + # TODO: Get this back in + # - name: Overwrite OptionsConfiguration for build-time initialization + # run: | + # $optionsPath = "samples/IntegrationTest/Assets/Scripts/OptionsConfiguration.cs" + # $content = Get-Content $optionsPath -Raw + # $content = $content -replace 'AndroidNativeInitializationType = NativeInitializationType.Runtime', 'AndroidNativeInitializationType = NativeInitializationType.BuildTime' + # Set-Content $optionsPath $content - - name: Export APK - Build-Time Initialization - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -CheckSymbols:$true -UnityVersion "$env:UNITY_VERSION" + # - name: Export APK - Build-Time Initialization + # run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -CheckSymbols:$true -UnityVersion "$env:UNITY_VERSION" + + # - name: Upload .apk + # uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + # with: + # name: testapp-android-compiled-${{ env.UNITY_VERSION }}-buildtime + # path: samples/IntegrationTest/Build/*.apk # Collect app but ignore the files that are not required for the test. + # retention-days: 14 # Lower retention period - we only need this to retry CI. - - name: Upload .apk - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: testapp-android-compiled-${{ env.UNITY_VERSION }}-buildtime - path: samples/IntegrationTest/Build/*.apk # Collect app but ignore the files that are not required for the test. - retention-days: 14 # Lower retention period - we only need this to retry CI. - - name: Upload IntegrationTest project on failure if: ${{ failure() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/.github/workflows/smoke-test-run-android.yml b/.github/workflows/smoke-test-run-android.yml index ddc508cc1..663dad69f 100644 --- a/.github/workflows/smoke-test-run-android.yml +++ b/.github/workflows/smoke-test-run-android.yml @@ -14,7 +14,7 @@ on: # Map the workflow outputs to job outputs outputs: status: - description: "Smoke test status" + description: "Integration test status" value: ${{ jobs.run.outputs.status }} defaults: @@ -28,14 +28,23 @@ jobs: env: ARTIFACTS_PATH: samples/IntegrationTest/test-artifacts/ HOMEBREW_NO_INSTALL_CLEANUP: 1 + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} # Map the job outputs to step outputs outputs: - status: ${{ steps.smoke-test.outputs.status }} - + status: ${{ steps.integration-test.outputs.status }} + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - + + - name: Initialize app-runner submodule + run: git submodule update --init modules/app-runner + shell: bash + + - name: Install Pester + run: Install-Module -Name Pester -Force -SkipPublisherCheck + - name: Download test app artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: @@ -56,9 +65,16 @@ jobs: mkdir -p $HOME/.android/avd touch $HOME/.android/repositories.cfg - - name: Run Android Smoke Tests + - name: Add Android build-tools to PATH + run: | + BUILD_TOOLS_DIR=$(ls -d $ANDROID_HOME/build-tools/*/ | sort -V | tail -1) + echo "Found build-tools at: $BUILD_TOOLS_DIR" + echo "$BUILD_TOOLS_DIR" >> $GITHUB_PATH + shell: bash + + - name: Run Android Integration Tests uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b # pin@v2.33.0 - id: smoke-test + id: integration-test timeout-minutes: 30 with: api-level: ${{ inputs.api-level }} @@ -81,12 +97,14 @@ jobs: adb wait-for-device adb shell input keyevent 82 adb devices -l - pwsh ./scripts/smoke-test-android.ps1 -WarnIfFlaky + pwsh -Command '$env:SENTRY_TEST_APK = "samples/IntegrationTest/Build/test.apk"; Invoke-Pester -Script test/IntegrationTest/Integration.Tests.ps1 -CI' - - name: Upload artifacts on failure + - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: testapp-android-logs-${{ inputs.api-level }}-${{ inputs.unity-version }} - path: ${{ env.ARTIFACTS_PATH }} - retention-days: 14 \ No newline at end of file + path: | + ${{ env.ARTIFACTS_PATH }} + test/IntegrationTest/results/ + retention-days: 14 diff --git a/.gitmodules b/.gitmodules index f761264dc..aaad81671 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "modules/sentry-native"] path = modules/sentry-native url = https://github.com/getsentry/sentry-native.git +[submodule "modules/app-runner"] + path = modules/app-runner + url = https://github.com/getsentry/app-runner.git diff --git a/modules/app-runner b/modules/app-runner new file mode 160000 index 000000000..0f3a63a67 --- /dev/null +++ b/modules/app-runner @@ -0,0 +1 @@ +Subproject commit 0f3a63a67aeead62b004e1d1548ffe47dc630fb6 diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index bb888bf4a..883bdc2aa 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -115,6 +115,7 @@ public void Init(SentryUnityOptions options) androidOptions.Call("setDsn", options.Dsn); androidOptions.Call("setDebug", options.Debug); androidOptions.Call("setRelease", options.Release); + androidOptions.Call("setDist", options.Distribution); androidOptions.Call("setEnvironment", options.Environment); var sentryLevelClass = new AndroidJavaClass("io.sentry.SentryLevel"); diff --git a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs index f524a753e..1c90cacb6 100644 --- a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs +++ b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs @@ -176,6 +176,12 @@ internal void ModifyManifest(string basePath) androidManifest.SetRelease(_options.Release); } + if (_options.Distribution is not null) + { + _logger.LogDebug("Setting Dist: {0}", _options.Distribution); + androidManifest.SetDist(_options.Distribution); + } + if (_options.Environment is not null) { _logger.LogDebug("Setting Environment: {0}", _options.Environment); @@ -469,6 +475,8 @@ internal void SetSampleRate(float sampleRate) => internal void SetRelease(string release) => SetMetaData($"{SentryPrefix}.release", release); + internal void SetDist(string dist) => SetMetaData($"{SentryPrefix}.dist", dist); + internal void SetAttachScreenshot(bool value) => SetMetaData($"{SentryPrefix}.attach-screenshot", value.ToString()); internal void SetEnvironment(string environment) => SetMetaData($"{SentryPrefix}.environment", environment); diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/SentryEditorWindowInstrumentation.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/SentryEditorWindowInstrumentation.cs index 5be965316..b64677096 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/SentryEditorWindowInstrumentation.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/SentryEditorWindowInstrumentation.cs @@ -50,6 +50,12 @@ private static void ConfigureOptions(Dictionary args, [CallerMem OptionsConfigurationItem.SetScript(value); } + if (args.TryGetValue("dsn", out value)) + { + Debug.LogFormat("{0}: Setting DSN", functionName); + options.Dsn = value; + } + optionsWindow.Close(); Debug.LogFormat("{0}: SUCCESS", functionName); } diff --git a/src/sentry-dotnet b/src/sentry-dotnet index 3a426e03f..d1f42b8c7 160000 --- a/src/sentry-dotnet +++ b/src/sentry-dotnet @@ -1 +1 @@ -Subproject commit 3a426e03f2bdd54b459ff5d2ba634d506e46e36e +Subproject commit d1f42b8c78b9ca36f3f73745287aaa3f3eba49b0 diff --git a/test/IntegrationTest/CommonTestCases.ps1 b/test/IntegrationTest/CommonTestCases.ps1 new file mode 100644 index 000000000..2bb1e1fa6 --- /dev/null +++ b/test/IntegrationTest/CommonTestCases.ps1 @@ -0,0 +1,131 @@ +# Defines a collection of reusable test cases that validate common Sentry event properties +# and behaviors across different test suites for consistent integration testing. +# +# Available parameters: +# - $TestSetup: Object containing test setup parameters +# - $TestType: String indicating the type of test being run (e.g., "crash-capture", "message-capture") +# - $SentryEvent: The Sentry event object retrieved from the REST API containing error/message details +# - $RunResult: Object containing the results of running the test application, including Output and ExitCode + +$CommonTestCases = @( + @{ Name = "Outputs event ID"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $eventId = Get-EventIds -appOutput $RunResult.Output -expectedCount 1 + $eventId | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Captures event in sentry.io"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Has title"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.title | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Has correct release version"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.release.version | Should -Be "sentry-unity-test@1.0.0" + } + } + @{ Name = "Has correct dist attribute"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.dist | Should -Be "test-dist" + } + } + @{ Name = "Has tags"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.tags | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Has correct integration test tags"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + ($SentryEvent.tags | Where-Object { $_.key -eq "test.suite" }).value | Should -Be "integration" + ($SentryEvent.tags | Where-Object { $_.key -eq "test.type" }).value | Should -Be $TestType + } + } + @{ Name = "Has correct environment tag"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + ($SentryEvent.tags | Where-Object { $_.key -eq "environment" }).value | Should -Be "integration-test" + } + } + @{ Name = "Contains user information"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + + if ($TestType -eq "crash-capture") { + # User context may not survive native crashes on all platforms + return + } + + $SentryEvent.user | Should -Not -BeNullOrEmpty + $SentryEvent.user.username | Should -Be "TestUser" + $SentryEvent.user.email | Should -Be "user-mail@test.abc" + $SentryEvent.user.id | Should -Be "12345" + } + } + @{ Name = "Contains breadcrumbs"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + + if ($TestType -eq "crash-capture") { + # Breadcrumbs may not survive native crashes + return + } + + $SentryEvent.breadcrumbs | Should -Not -BeNullOrEmpty + $SentryEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains expected breadcrumbs"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + + if ($TestType -eq "crash-capture") { + return + } + + $SentryEvent.breadcrumbs.values | Should -Not -BeNullOrEmpty + $SentryEvent.breadcrumbs.values | Where-Object { $_.message -eq "Integration test started" } | Should -Not -BeNullOrEmpty + $SentryEvent.breadcrumbs.values | Where-Object { $_.message -eq "Context configuration finished" } | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains SDK information"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.sdk | Should -Not -BeNullOrEmpty + $SentryEvent.sdk.name | Should -Not -BeNullOrEmpty + $SentryEvent.sdk.version | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains app context"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + + if ($TestType -eq "crash-capture") { + # App context may not be available for native crashes + return + } + + $SentryEvent.contexts.app | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains device context"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.contexts.device | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains OS context"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + $SentryEvent.contexts.os | Should -Not -BeNullOrEmpty + $SentryEvent.contexts.os.name | Should -Not -BeNullOrEmpty + } + } + @{ Name = "Contains Unity context"; TestBlock = { + param($TestSetup, $TestType, $SentryEvent, $RunResult) + + if ($TestType -eq "crash-capture") { + # Unity context may not be synchronized to NDK for native crashes + return + } + + $SentryEvent.contexts.unity | Should -Not -BeNullOrEmpty + } + } +) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 new file mode 100644 index 000000000..05c1fd8c1 --- /dev/null +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -0,0 +1,196 @@ +#!/usr/bin/env pwsh +# +# Integration tests for Sentry Unity SDK (Android) +# +# Environment variables: +# SENTRY_TEST_APK: path to the test APK file +# SENTRY_TEST_DSN: test DSN +# SENTRY_AUTH_TOKEN: authentication token for Sentry API + +Set-StrictMode -Version latest +$ErrorActionPreference = "Stop" + +# Import app-runner modules +. $PSScriptRoot/../../modules/app-runner/import-modules.ps1 + +# Import shared test cases and utility functions +. $PSScriptRoot/CommonTestCases.ps1 + + +BeforeAll { + $script:AndroidComponent = "io.sentry.unity.integrationtest/com.unity3d.player.UnityPlayerActivity" + $script:FallbackAndroidComponent = "io.sentry.unity.integrationtest/com.unity3d.player.UnityPlayerGameActivity" + + # Run integration test action on device + function Invoke-TestAction { + param ( + [Parameter(Mandatory=$true)] + [string]$Action + ) + + Write-Host "Running $Action..." + + $extras = @("-e", "test", $Action) + + try { + $runResult = Invoke-DeviceApp -ExecutablePath $script:AndroidComponent -Arguments $extras + } + catch { + if ($_.Exception.Message -match "Activity class .* does not exist" -or $_.Exception.Message -match "Error type 3") { + Write-Host "Activity not found, trying fallback: $($script:FallbackAndroidComponent)" + $script:AndroidComponent = $script:FallbackAndroidComponent + $runResult = Invoke-DeviceApp -ExecutablePath $script:AndroidComponent -Arguments $extras + } + else { + throw + } + } + + # Save result to JSON file + $runResult | ConvertTo-Json -Depth 5 | Out-File -FilePath (Get-OutputFilePath "${Action}-result.json") + + # Launch app again to ensure crash report is sent + if ($Action -eq "crash-capture") { + Write-Host "Running crash-send to ensure crash report is sent..." + + $sendExtras = @("-e", "test", "crash-send") + Invoke-DeviceApp -ExecutablePath $script:AndroidComponent -Arguments $sendExtras + } + + # Print app output so it's visible in CI logs + Write-Host "::group::App output ($Action)" + $runResult.Output | ForEach-Object { Write-Host $_ } + Write-Host "::endgroup::" + + return $runResult + } + + # Create directory for the test results + New-Item -ItemType Directory -Path "$PSScriptRoot/results/" -ErrorAction Continue 2>&1 | Out-Null + Set-OutputDir -Path "$PSScriptRoot/results/" + + # Initialize test parameters + $script:TestSetup = [PSCustomObject]@{ + ApkPath = $env:SENTRY_TEST_APK + Dsn = $env:SENTRY_TEST_DSN + AuthToken = $env:SENTRY_AUTH_TOKEN + } + + # Validate environment + if ([string]::IsNullOrEmpty($script:TestSetup.ApkPath)) { + throw "SENTRY_TEST_APK environment variable is not set." + } + if (-not (Test-Path $script:TestSetup.ApkPath)) { + throw "APK not found at: $($script:TestSetup.ApkPath)" + } + if ([string]::IsNullOrEmpty($script:TestSetup.Dsn)) { + throw "SENTRY_TEST_DSN environment variable is not set." + } + if ([string]::IsNullOrEmpty($script:TestSetup.AuthToken)) { + throw "SENTRY_AUTH_TOKEN environment variable is not set." + } + + Connect-SentryApi ` + -ApiToken $script:TestSetup.AuthToken ` + -DSN $script:TestSetup.Dsn + + Connect-Device -Platform "Adb" + Install-DeviceApp -Path $script:TestSetup.ApkPath +} + + +AfterAll { + Disconnect-SentryApi + Disconnect-Device +} + + +Describe "Unity Android Integration Tests" { + + Context "Message Capture" { + BeforeAll { + $script:runResult = Invoke-TestAction -Action "message-capture" + + $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 + if ($eventId) { + Write-Host "::group::Getting event content" + $script:runEvent = Get-SentryTestEvent -EventId "$eventId" + Write-Host "::endgroup::" + } + } + + It "" -ForEach $CommonTestCases { + & $testBlock -SentryEvent $runEvent -TestType "message-capture" -RunResult $runResult -TestSetup $script:TestSetup + } + + It "Has message level info" { + ($runEvent.tags | Where-Object { $_.key -eq "level" }).value | Should -Be "info" + } + + It "Has message content" { + $runEvent.title | Should -Not -BeNullOrEmpty + } + } + + Context "Exception Capture" { + BeforeAll { + $script:runResult = Invoke-TestAction -Action "exception-capture" + + $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 + if ($eventId) { + Write-Host "::group::Getting event content" + $script:runEvent = Get-SentryTestEvent -EventId "$eventId" + Write-Host "::endgroup::" + } + } + + It "" -ForEach $CommonTestCases { + & $testBlock -SentryEvent $runEvent -TestType "exception-capture" -RunResult $runResult -TestSetup $script:TestSetup + } + + It "Has exception information" { + $runEvent.exception | Should -Not -BeNullOrEmpty + $runEvent.exception.values | Should -Not -BeNullOrEmpty + } + + It "Has exception with stacktrace" { + $exception = $runEvent.exception.values[0] + $exception | Should -Not -BeNullOrEmpty + $exception.type | Should -Not -BeNullOrEmpty + $exception.stacktrace | Should -Not -BeNullOrEmpty + } + + It "Has error level" { + ($runEvent.tags | Where-Object { $_.key -eq "level" }).value | Should -Be "error" + } + } + + Context "Crash Capture" { + BeforeAll { + $script:runResult = Invoke-TestAction -Action "crash-capture" + + $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 + if ($eventId) { + Write-Host "::group::Getting event content" + $script:runEvent = Get-SentryTestEvent -TagName "test.crash_id" -TagValue "$eventId" -TimeoutSeconds 120 + Write-Host "::endgroup::" + } + } + + It "" -ForEach $CommonTestCases { + & $testBlock -SentryEvent $runEvent -TestType "crash-capture" -RunResult $runResult -TestSetup $script:TestSetup + } + + It "Has fatal level" { + ($runEvent.tags | Where-Object { $_.key -eq "level" }).value | Should -Be "fatal" + } + + It "Has exception with stacktrace" { + $runEvent.exception | Should -Not -BeNullOrEmpty + $runEvent.exception.values | Should -Not -BeNullOrEmpty + $exception = $runEvent.exception.values[0] + $exception | Should -Not -BeNullOrEmpty + $exception.stacktrace | Should -Not -BeNullOrEmpty + } + } +} diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 7cc900ecc..bb2ae779c 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -29,16 +29,15 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, DisableUnityAudio(); DisableProgressiveLightMapper(); - // This should make IL2CCPP builds faster, see https://forum.unity.com/threads/il2cpp-build-time-improvements-seeking-feedback.1064135/ - Debug.Log("Builder: Setting IL2CPP generation to OptimizeSize"); + Debug.Log("Builder: Setting IL2CPP generation to OptimizeSpeed"); #if UNITY_2022_1_OR_NEWER - PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group), UnityEditor.Build.Il2CppCodeGeneration.OptimizeSize); + PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group), UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed); #elif UNITY_2021_2_OR_NEWER - EditorUserBuildSettings.il2CppCodeGeneration = UnityEditor.Build.Il2CppCodeGeneration.OptimizeSize; + EditorUserBuildSettings.il2CppCodeGeneration = UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed; #endif Debug.Log("Builder: Configuring code stripping level"); -#if UNITY_2022_1_OR_NEWER +#if UNITY_6000_0_OR_NEWER PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.FromBuildTargetGroup(group), ManagedStrippingLevel.High); #else PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.FromBuildTargetGroup(group), ManagedStrippingLevel.Low); @@ -137,13 +136,12 @@ public static void BuildAndroidIl2CPPPlayer() { Debug.Log("Builder: Building Android IL2CPP Player"); -#if UNITY_6000_3_OR_NEWER - // Force OpenGLES3 to avoid Vulkan emulator crashes in CI. - // The Android emulator's swiftshader Vulkan implementation has shutdown issues - // with Unity 6000.3+ that cause SIGSEGV in libvulkan_enc.so after tests complete. + // Force OpenGLES3 to avoid Vulkan issues with the Android emulator in CI. + // The emulator's swiftshader Vulkan implementation doesn't fully support Unity's + // Vulkan usage, causing "Processed some Vulkan packets without process resources + // created" warnings and SIGSEGV crashes in libvulkan_enc.so. PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.Android, false); PlayerSettings.SetGraphicsAPIs(BuildTarget.Android, new[] { UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3 }); -#endif #if UNITY_2021_2_OR_NEWER && !UNITY_6000_0_OR_NEWER // Clean Android gradle cache to force regeneration of gradle files diff --git a/test/Scripts.Integration.Test/Scenes/SmokeTest.unity b/test/Scripts.Integration.Test/Scenes/SmokeTest.unity index d9c403c56..86e603ce6 100644 --- a/test/Scripts.Integration.Test/Scenes/SmokeTest.unity +++ b/test/Scripts.Integration.Test/Scenes/SmokeTest.unity @@ -379,7 +379,7 @@ MonoBehaviour: m_GameObject: {fileID: 1185210226} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 6b8ba3d687233471198b184bbcb4fdbd, type: 3} + m_Script: {fileID: 11500000, guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6, type: 3} m_Name: m_EditorClassIdentifier: --- !u!1 &1263241751 diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs new file mode 100644 index 000000000..7f8352899 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Sentry; +using Sentry.Unity; +using UnityEngine; + +public class IntegrationOptionsConfiguration : SentryOptionsConfiguration +{ + public override void Configure(SentryUnityOptions options) + { + Debug.Log("Sentry: IntegrationOptionsConfig::Configure() called"); + + // DSN is baked into SentryOptions.asset at build time by configure-sentry.ps1 + // which passes the SENTRY_DSN env var to ConfigureOptions via the -dsn argument. + + options.Environment = "integration-test"; + options.Release = "sentry-unity-test@1.0.0"; + options.Distribution = "test-dist"; + + options.AttachScreenshot = true; + options.Debug = true; + options.DiagnosticLevel = SentryLevel.Debug; + options.TracesSampleRate = 1.0d; + + // No custom HTTP handler -- events go to real sentry.io + + // Filtering test output from breadcrumbs + options.AddBreadcrumbsForLogType = new Dictionary + { + { LogType.Error, true }, + { LogType.Assert, true }, + { LogType.Warning, true }, + { LogType.Log, false }, + { LogType.Exception, true }, + }; + + // Disable ANR to avoid test interference + options.DisableAnrIntegration(); + + // Runtime initialization for integration tests + options.AndroidNativeInitializationType = NativeInitializationType.Runtime; + options.IosNativeInitializationType = NativeInitializationType.Runtime; + + Debug.Log("Sentry: IntegrationOptionsConfig::Configure() finished"); + } +} diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs.meta b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs.meta new file mode 100644 index 000000000..271393dc0 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs new file mode 100644 index 000000000..14c7f65a3 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Sentry; +using Sentry.Unity; +using UnityEngine; +using UnityEngine.Diagnostics; + +public class IntegrationTester : MonoBehaviour +{ + public void Start() + { + var arg = TestLauncher.GetTestArg(); + Debug.Log($"IntegrationTester arg: '{arg}'"); + + switch (arg) + { + case "message-capture": + MessageCapture(); + break; + case "exception-capture": + ExceptionCapture(); + break; + case "crash-capture": + CrashCapture(); + break; + case "crash-send": + CrashSend(); + break; + default: + Debug.LogError($"IntegrationTester: Unknown command: {arg}"); + Application.Quit(1); + break; + } + } + + private void AddIntegrationTestContext(string testType) + { + SentrySdk.AddBreadcrumb("Integration test started"); + + SentrySdk.ConfigureScope(scope => + { + scope.SetTag("test.suite", "integration"); + scope.SetTag("test.type", testType); + scope.User = new SentryUser + { + Id = "12345", + Username = "TestUser", + Email = "user-mail@test.abc" + }; + }); + + SentrySdk.AddBreadcrumb("Context configuration finished"); + } + + private void MessageCapture() + { + AddIntegrationTestContext("message-capture"); + + var eventId = SentrySdk.CaptureMessage("Integration test message"); + Debug.Log($"EVENT_CAPTURED: {eventId}"); + + Application.Quit(0); + } + + private void ExceptionCapture() + { + AddIntegrationTestContext("exception-capture"); + + try + { + DoSomeWork(); + } + catch (Exception ex) + { + var eventId = SentrySdk.CaptureException(ex); + Debug.Log($"EVENT_CAPTURED: {eventId}"); + } + + Application.Quit(0); + } + + // Use a deeper call stack with NoInlining to ensure Unity 2022's IL2CPP + // produces a non-empty managed stack trace (single-method throw/catch can + // result in an empty stack trace with OptimizeSize + High stripping). + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DoSomeWork() + { + if (DateTime.Now.Ticks > 0) // Always true but not optimizable + { + ThrowException(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowException() + { + throw new InvalidOperationException("Integration test exception"); + } + + private void CrashCapture() + { + var crashId = Guid.NewGuid().ToString(); + + AddIntegrationTestContext("crash-capture"); + + SentrySdk.ConfigureScope(scope => + { + scope.SetTag("test.crash_id", crashId); + }); + + Debug.Log($"EVENT_CAPTURED: {crashId}"); + Debug.Log("CRASH TEST: Issuing a native crash (Abort)"); + + Utils.ForceCrash(ForcedCrashCategory.Abort); + + // Should not reach here + Debug.LogError("CRASH TEST: FAIL - unexpected code executed after crash"); + Application.Quit(1); + } + + private void CrashSend() + { + Debug.Log("CrashSend: Initializing Sentry to flush cached crash report..."); + + // Sentry is already initialized by IntegrationOptionsConfiguration. + // Just wait a bit for the queued crash report to be sent, then quit. + StartCoroutine(WaitAndQuit()); + } + + private IEnumerator WaitAndQuit() + { + // Wait for the crash report to be sent + yield return new WaitForSeconds(10); + + SentrySdk.FlushAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); + + Debug.Log("CrashSend: Flush complete, quitting."); + Application.Quit(0); + } +} diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs.meta b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs.meta new file mode 100644 index 000000000..dc7b008b6 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs index 3657b5039..fb88633e6 100644 --- a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs +++ b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs @@ -126,9 +126,6 @@ private IEnumerator SmokeTestCoroutine() t.ExpectMessage(currentMessage, "'type':'transaction"); t.ExpectMessage(currentMessage, "'op':'app.start'"); // startup transaction -#if !UNITY_EDITOR - t.ExpectMessage(currentMessage, "'op':'awake','description':'Main Camera.SmokeTester'"); // auto instrumentation -#endif t.ExpectMessageNot(currentMessage, "'length':0"); var guid = Guid.NewGuid().ToString(); diff --git a/test/Scripts.Integration.Test/Scripts/TestLauncher.cs b/test/Scripts.Integration.Test/Scripts/TestLauncher.cs new file mode 100644 index 000000000..e98281705 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/TestLauncher.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.InteropServices; +using UnityEngine; + +#if UNITY_WEBGL +using System.Web; +#endif + +public class TestLauncher : MonoBehaviour +{ + private void Awake() + { + Debug.Log("TestLauncher, awake!"); + Application.quitting += () => + { + // Keep "SmokeTester is quitting." for backward compatibility with smoke-test-android.ps1 + // and run-smoke-test.ps1 which look for this exact string to detect test completion. + Debug.Log("SmokeTester is quitting."); + }; + } + + public void Start() + { + var arg = GetTestArg(); + Debug.Log($"TestLauncher arg: '{arg}'"); + + switch (arg) + { + // Legacy smoke test commands -> SmokeTester + case "smoke": + case "crash": + case "has-crashed": + case "hasnt-crashed": + gameObject.AddComponent(); + break; + + // Integration test commands -> IntegrationTester + case "message-capture": + case "exception-capture": + case "crash-capture": + case "crash-send": + gameObject.AddComponent(); + break; + + default: + Debug.LogError($"Unknown test command: {arg}"); + Application.Quit(1); + break; + } + } + +#if UNITY_IOS && !UNITY_EDITOR + [DllImport("__Internal", EntryPoint="getTestArgObjectiveC")] + internal static extern string GetTestArg(); +#else + internal static string GetTestArg() + { + string arg = null; +#if UNITY_EDITOR +#elif UNITY_ANDROID + using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var currentActivity = unityPlayer.GetStatic("currentActivity")) + using (var intent = currentActivity.Call("getIntent")) + { + arg = intent.Call("getStringExtra", "test"); + } +#elif UNITY_WEBGL + var uri = new Uri(Application.absoluteURL); + arg = HttpUtility.ParseQueryString(uri.Query).Get("test"); +#else + var args = Environment.GetCommandLineArgs(); + if (args.Length > 2 && args[1] == "--test") + { + arg = args[2]; + } +#endif + return arg; + } +#endif +} diff --git a/test/Scripts.Integration.Test/Scripts/TestLauncher.cs.meta b/test/Scripts.Integration.Test/Scripts/TestLauncher.cs.meta new file mode 100644 index 000000000..69999fcd4 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/TestLauncher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test/Scripts.Integration.Test/configure-sentry.ps1 b/test/Scripts.Integration.Test/configure-sentry.ps1 index 332716a22..daa9d4bb1 100644 --- a/test/Scripts.Integration.Test/configure-sentry.ps1 +++ b/test/Scripts.Integration.Test/configure-sentry.ps1 @@ -1,7 +1,9 @@ param( [string] $UnityPath, [string] $Platform = "", - [Switch] $CheckSymbols + [Switch] $CheckSymbols, + [ValidateSet("smoke", "integration")] + [string] $TestMode = "smoke" ) if (-not $Global:NewProjectPathCache) @@ -13,15 +15,21 @@ if (-not $Global:NewProjectPathCache) $UnityPath = FormatUnityPath $UnityPath -Write-Log "Configuring Sentry options..." +Write-Log "Configuring Sentry options (TestMode: $TestMode)..." + +$optionsScript = if ($TestMode -eq "integration") { "IntegrationOptionsConfiguration" } else { "OptionsConfiguration" } $unityArgs = @( ` "-quit", "-batchmode", "-nographics", "-disable-assembly-updater", "-projectPath ", $(GetNewProjectPath), ` "-executeMethod", "Sentry.Unity.Editor.ConfigurationWindow.SentryEditorWindowInstrumentation.ConfigureOptions", ` - "-optionsScript", "OptionsConfiguration", ` + "-optionsScript", $optionsScript, ` "-cliOptionsScript", "CliConfiguration", ` "-cliOptions.UrlOverride", ($CheckSymbols ? (SymbolServerUrlFor $UnityPath $Platform) : "") ) +if ($env:SENTRY_DSN) { + $unityArgs += @("-dsn", $env:SENTRY_DSN) +} + RunUnityAndExpect $UnityPath "ConfigureSentryOptions" "ConfigureOptions: SUCCESS" $unityArgs diff --git a/test/Scripts.Integration.Test/run-smoke-test.ps1 b/test/Scripts.Integration.Test/run-smoke-test.ps1 index 5942a3827..59c3c36ad 100644 --- a/test/Scripts.Integration.Test/run-smoke-test.ps1 +++ b/test/Scripts.Integration.Test/run-smoke-test.ps1 @@ -106,9 +106,20 @@ function RunTest([string] $type) Write-Log "$type test: Player.log contents END" -ForegroundColor Yellow } + # Check for test failures first - a graceful shutdown doesn't mean tests passed. + $lineWithFailure = $appLog | Select-String "$($type.ToUpper()) TEST: FAIL" + If ($lineWithFailure) + { + $info = "Test process finished with status code $($process.ExitCode). $lineWithFailure" + If ($type -ne "crash") + { + throw $info + } + Write-Log $info + } # Relying on ExitCode does not seem reliable. We're looking for the line "SmokeTester is quitting." instead to indicate # a successful shut-down. - If ($appLog | Select-String "SmokeTester is quitting.") + ElseIf ($appLog | Select-String "SmokeTester is quitting.") { Write-Log "$type test: PASSED" -ForegroundColor Green } @@ -119,8 +130,7 @@ function RunTest([string] $type) } Else { - $lineWithFailure = $appLog | Select-String "$($type.ToUpper()) TEST: FAIL" - $info = "Test process finished with status code $($process.ExitCode). $lineWithFailure" + $info = "Test process finished with status code $($process.ExitCode). No completion marker found in Player.log" If ($type -ne "crash") { throw $info