diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56117b6ba..b56c5bab8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] uses: ./.github/workflows/smoke-test-create.yml with: unity-version: ${{ matrix.unity-version }} @@ -101,7 +101,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] platform: ["WebGL", "Linux"] include: - platform: WebGL @@ -205,7 +205,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] uses: ./.github/workflows/smoke-test-build-android.yml with: unity-version: ${{ matrix.unity-version }} @@ -224,7 +224,7 @@ jobs: matrix: api-level: [30, 31, 34] # last updated January 2025 init-type: ["runtime", "buildtime"] - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] smoke-test-build-ios: name: Build iOS ${{ matrix.unity-version }} Smoke Test @@ -234,7 +234,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] uses: ./.github/workflows/smoke-test-build-ios.yml with: unity-version: ${{ matrix.unity-version }} @@ -247,7 +247,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] init-type: ["runtime", "buildtime"] uses: ./.github/workflows/smoke-test-compile-ios.yml with: @@ -266,7 +266,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] # Check https://support.apple.com/en-us/HT201222 for the latest minor version for a given major one. # https://developer.apple.com/support/app-store/ shows that of all iOS devices # - `iOS 17`: 86 % @@ -287,7 +287,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] platform: ["WebGL", "Linux"] steps: - name: Checkout @@ -325,7 +325,7 @@ jobs: strategy: fail-fast: false matrix: - unity-version: ["2020", "2022", "6000"] + unity-version: ["2020", "2022", "6000", "6100"] # os: ["windows", "macos"] os: ["windows"] include: diff --git a/CHANGELOG.md b/CHANGELOG.md index 77abdc420..3ad301820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ customize it by creating your own variant. The user feedback feature allows your users to provide feedback in form of a written message that can optionally have a screenshot attached. Read more about it ([here](https://docs.sentry.io/product/user-feedback/)). ([#2220](https://github.com/getsentry/sentry-unity/pull/2220)) +### Fixes + +- The SDK now waits for 'End of Frame' before capturing a screenshot. This should address any blank or malformed + screenshots previously attached to events. The SDK now also only captures one screenshot for the first error event in + each individual frame. ([#2240](https://github.com/getsentry/sentry-unity/pull/2240)) + ### Dependencies - Bump Java SDK from v8.14.0 to v8.17.0 ([#2218](https://github.com/getsentry/sentry-unity/pull/2218), [#2223](https://github.com/getsentry/sentry-unity/pull/2223), [#2238](https://github.com/getsentry/sentry-unity/pull/2238)) diff --git a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset index 153017e4e..831e829b4 100644 --- a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset +++ b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset @@ -28,10 +28,10 @@ MonoBehaviour: k__BackingField: k__BackingField: k__BackingField: 1 - k__BackingField: 0 + k__BackingField: 1 k__BackingField: 1 k__BackingField: 75 - k__BackingField: 0 + k__BackingField: 1 k__BackingField: 100 k__BackingField: 20 k__BackingField: 10 diff --git a/scripts/ci-env.ps1 b/scripts/ci-env.ps1 index 448f9c53b..a541243f3 100644 --- a/scripts/ci-env.ps1 +++ b/scripts/ci-env.ps1 @@ -19,6 +19,9 @@ switch ($name) { "unity6000" { return "6000.0.49f1" } + "unity6100" { + return "6000.1.12f1" + } Default { throw "Unkown variable '$name'" } diff --git a/src/Sentry.Unity/ScreenshotEventProcessor.cs b/src/Sentry.Unity/ScreenshotEventProcessor.cs index a8b8e3584..4d960cf8c 100644 --- a/src/Sentry.Unity/ScreenshotEventProcessor.cs +++ b/src/Sentry.Unity/ScreenshotEventProcessor.cs @@ -1,64 +1,69 @@ +using System; +using System.Collections; using Sentry.Extensibility; -using Sentry.Unity.Integrations; +using Sentry.Internal; using UnityEngine; namespace Sentry.Unity; -public class ScreenshotEventProcessor : ISentryEventProcessorWithHint +public class ScreenshotEventProcessor : ISentryEventProcessor { private readonly SentryUnityOptions _options; - private readonly IApplication _application; - public ScreenshotEventProcessor(SentryUnityOptions sentryOptions) : this(sentryOptions, null) { } + private readonly ISentryMonoBehaviour _sentryMonoBehaviour; + private bool _isCapturingScreenshot; - internal ScreenshotEventProcessor(SentryUnityOptions sentryOptions, IApplication? application) + internal Func ScreenshotCaptureFunction = SentryScreenshot.Capture; + internal Action AttachmentCaptureFunction = (eventId, attachment) => + ((Hub)Sentry.SentrySdk.CurrentHub).CaptureAttachment(eventId, attachment); + internal Func WaitForEndOfFrameFunction = () => new WaitForEndOfFrame(); + + public ScreenshotEventProcessor(SentryUnityOptions sentryOptions) : this(sentryOptions, SentryMonoBehaviour.Instance) { } + + internal ScreenshotEventProcessor(SentryUnityOptions sentryOptions, ISentryMonoBehaviour sentryMonoBehaviour) { _options = sentryOptions; - _application = application ?? ApplicationAdapter.Instance; + _sentryMonoBehaviour = sentryMonoBehaviour; } - public SentryEvent? Process(SentryEvent @event) + public SentryEvent Process(SentryEvent @event) { + // Only ever capture one screenshot per frame + if (!_isCapturingScreenshot) + { + _isCapturingScreenshot = true; + _sentryMonoBehaviour.StartCoroutine(CaptureScreenshotCoroutine(@event.EventId)); + } return @event; } - public SentryEvent? Process(SentryEvent @event, SentryHint hint) + internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) { - // save event id - // wait for end of frame - // check if last id is event it - // send screenshot + _options.LogDebug("Screenshot capture triggered. Waiting for End of Frame."); - // add workitem: screentshot for ID xxx - // sdk integration checking for work: if ID got sent, follow up with screenshot + // WaitForEndOfFrame does not work in headless mode so we're making it configurable for CI. + // See https://docs.unity3d.com/6000.1/Documentation/ScriptReference/WaitForEndOfFrame.html + yield return WaitForEndOfFrameFunction(); - if (!MainThreadData.IsMainThread()) + try { - _options.DiagnosticLogger?.LogDebug("Screenshot capture skipped. Can't capture screenshots on other than the main thread."); - return @event; - } + var screenshotBytes = ScreenshotCaptureFunction(_options); + var attachment = new SentryAttachment( + AttachmentType.Default, + new ByteAttachmentContent(screenshotBytes), + "screenshot.jpg", + "image/jpeg"); - if (_options.BeforeCaptureScreenshotInternal?.Invoke() is not false) - { - if (_application.IsEditor) - { - _options.DiagnosticLogger?.LogInfo("Screenshot capture skipped. Capturing screenshots it not supported in the Editor"); - return @event; - } + _options.LogDebug("Screenshot captured for event {0}", eventId); - if (Screen.width == 0 || Screen.height == 0) - { - _options.DiagnosticLogger?.LogWarning("Can't capture screenshots on a screen with a resolution of '{0}x{1}'.", Screen.width, Screen.height); - } - else - { - hint.AddAttachment(SentryScreenshot.Capture(_options), "screenshot.jpg", contentType: "image/jpeg"); - } + AttachmentCaptureFunction(eventId, attachment); } - else + catch (Exception e) { - _options.DiagnosticLogger?.LogInfo("Screenshot capture skipped by BeforeAttachScreenshot callback."); + _options.LogError(e, "Failed to capture screenshot."); + } + finally + { + _isCapturingScreenshot = false; } - - return @event; } } diff --git a/src/Sentry.Unity/SentryMonoBehaviour.cs b/src/Sentry.Unity/SentryMonoBehaviour.cs index 0945d3e72..4b1ff5f88 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using Sentry.Unity.Integrations; using UnityEngine; @@ -7,6 +8,7 @@ namespace Sentry.Unity; internal interface ISentryMonoBehaviour { event Action? ApplicationResuming; + public Coroutine StartCoroutine(IEnumerator routine); } /// diff --git a/src/Sentry.Unity/SentrySdk.cs b/src/Sentry.Unity/SentrySdk.cs index 55cb951bb..65d15e63d 100644 --- a/src/Sentry.Unity/SentrySdk.cs +++ b/src/Sentry.Unity/SentrySdk.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using Sentry.Extensibility; -using Sentry.Unity.NativeUtils; namespace Sentry.Unity; diff --git a/src/Sentry.Unity/SentryUnitySdk.cs b/src/Sentry.Unity/SentryUnitySdk.cs index c590cbbda..7fdeed66e 100644 --- a/src/Sentry.Unity/SentryUnitySdk.cs +++ b/src/Sentry.Unity/SentryUnitySdk.cs @@ -2,6 +2,7 @@ using System.IO; using System.Threading.Tasks; using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Unity.Integrations; using UnityEngine; @@ -126,6 +127,26 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad Sentry.SentrySdk.CurrentHub.CaptureFeedback(message, email, name, hint: hint); } + internal void CaptureAttachment(SentryId eventId, SentryAttachment attachment) + { + try + { + if (Sentry.SentrySdk.CurrentHub is Hub hub) + { + hub.CaptureAttachment(eventId, attachment); + _options.LogDebug("Attachment captured for event {0}", eventId); + } + else + { + _options.LogError("Capturing the attachment failed due to the current hub."); + } + } + catch (Exception ex) + { + _options.DiagnosticLogger?.LogError(ex, "Failed to capture attachment for event {0}", eventId); + } + } + internal static void SetUpWindowsPlayerCaching(SentryUnitySdk unitySdk, SentryUnityOptions options) { // On Windows-Standalone, we disable cache dir in case multiple app instances run over the same path. diff --git a/src/sentry-dotnet b/src/sentry-dotnet index 57901f427..25a894dd4 160000 --- a/src/sentry-dotnet +++ b/src/sentry-dotnet @@ -1 +1 @@ -Subproject commit 57901f42726dcca3232fe5bee9c3f7ff41646b1f +Subproject commit 25a894dd4e295bcb486de055ca547b1eb2fc270b diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index e2955b439..09481edd5 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -41,14 +41,6 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, PlayerSettings.SetManagedStrippingLevel(group, ManagedStrippingLevel.Low); #endif - - // This is a workaround for build issues with Unity 2022.3. and newer. - // https://discussions.unity.com/t/gradle-build-issues-for-android-api-sdk-35-in-unity-2022-3lts/1502187/10 -#if UNITY_2022_3_OR_NEWER - Debug.Log("Builder: Setting Android target API level to 33"); - PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevel33; -#endif - Debug.Log("Builder: Updating BuildPlayerOptions"); var buildPlayerOptions = new BuildPlayerOptions { diff --git a/test/Scripts.Integration.Test/Scripts/OptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/OptionsConfiguration.cs index 0f8bccf5b..ffe29a116 100644 --- a/test/Scripts.Integration.Test/Scripts/OptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/OptionsConfiguration.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Sentry; using Sentry.Unity; using UnityEngine; @@ -37,6 +38,16 @@ public override void Configure(SentryUnityOptions options) return -2; }; + // Filtering the SmokeTester logs from the breadcrumbs here + options.AddBreadcrumbsForLogType = new Dictionary + { + { LogType.Error, true}, + { LogType.Assert, true}, + { LogType.Warning, true}, + { LogType.Log, false}, // No breadcrumbs for Debug.Log + { LogType.Exception, true}, + }; + // If an ANR triggers while the smoke test runs, the test would fail because we expect exact order of events. options.DisableAnrIntegration(); diff --git a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs index 16becdfdf..f575e381b 100644 --- a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs +++ b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Concurrent; using System.Linq; @@ -92,9 +93,10 @@ private static string GetTestArg() internal static Func CrashedLastRun = () => -1; - public static void SmokeTest() + private static void SmokeTest() { t.Start("SMOKE"); + try { #if !UNITY_EDITOR @@ -106,7 +108,7 @@ public static void SmokeTest() // Skip the session init requests (there may be multiple of them). We can't skip them by a "positive" // because they're also repeated with standard events (in an envelope). - Debug.Log("SMOKE TEST: Skipping all session requests"); + Debug.Log("Skipping all session requests"); for (; currentMessage < 10; currentMessage++) { if (t.CheckMessage(currentMessage, "'type':'transaction'")) @@ -114,7 +116,7 @@ public static void SmokeTest() break; } } - Debug.Log($"SMOKE TEST: Done skipping session requests. Last one was: #{currentMessage}"); + Debug.Log($"Done skipping session requests. Last one was: #{currentMessage}"); t.ExpectMessage(currentMessage, "'type':'transaction"); t.ExpectMessage(currentMessage, "'op':'app.start'"); // startup transaction @@ -122,10 +124,11 @@ public static void SmokeTest() t.ExpectMessage(currentMessage, "'op':'awake','description':'Main Camera.SmokeTester'"); // auto instrumentation #endif t.ExpectMessageNot(currentMessage, "'length':0"); - + var guid = Guid.NewGuid().ToString(); Debug.LogError($"LogError(GUID)={guid}"); - currentMessage++; + + currentMessage++; // The error event t.ExpectMessage(currentMessage, "'type':'event'"); t.ExpectMessage(currentMessage, $"LogError(GUID)={guid}"); @@ -138,31 +141,46 @@ public static void SmokeTest() t.ExpectMessage(currentMessage, "'type':'unity',"); // User t.ExpectMessage(currentMessage, "'user':{'id':'"); // non-null automatic ID - // Attachment + t.ExpectMessageNot(currentMessage, "'length':0"); + + currentMessage++; // The screenshot envelope + t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); t.ExpectMessageNot(currentMessage, "'length':0"); SentrySdk.CaptureMessage($"CaptureMessage(GUID)={guid}"); - currentMessage++; + + currentMessage++; // The message event t.ExpectMessage(currentMessage, "'type':'event'"); t.ExpectMessage(currentMessage, $"CaptureMessage(GUID)={guid}"); + t.ExpectMessageNot(currentMessage, "'length':0"); + + currentMessage++; // The screenshot envelope + t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); t.ExpectMessageNot(currentMessage, "'length':0"); var ex = new Exception("Exception & context test"); AddContext(); SentrySdk.CaptureException(ex); - t.ExpectMessage(++currentMessage, "'type':'event'"); + + currentMessage++; // The exception event + + t.ExpectMessage(currentMessage, "'type':'event'"); t.ExpectMessage(currentMessage, "'message':'crumb','type':'error','data':{'foo':'bar'},'category':'bread','level':'critical'}"); t.ExpectMessage(currentMessage, "'message':'scope-crumb'}"); t.ExpectMessage(currentMessage, "'extra':{'extra-key':42}"); t.ExpectMessage(currentMessage, "'tag-key':'tag-value'"); t.ExpectMessage(currentMessage, "'user':{'id':'user-id','username':'username','email':'email@example.com','ip_address':'::1','other':{'role':'admin'}}"); + t.ExpectMessageNot(currentMessage, "'length':0"); + + currentMessage++; // The screenshot envelope + t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); t.ExpectMessageNot(currentMessage, "'length':0"); - Debug.Log("SMOKE TEST: Finished checking messages."); + Debug.Log("Finished checking messages."); t.Pass(); } @@ -239,7 +257,7 @@ internal class TestHandler : HttpClientHandler private ConcurrentQueue _requests = new ConcurrentQueue(); private AutoResetEvent _requestReceived = new AutoResetEvent(false); - private readonly TimeSpan _receiveTimeout = TimeSpan.FromSeconds(10); + private readonly TimeSpan _receiveTimeout = TimeSpan.FromSeconds(30); // Screenshot capture happens in a coroutine and takes ~10+ seconds private int _testNumber = 0; public int ExitCode = 0; @@ -259,10 +277,7 @@ protected override Task SendAsync(HttpRequestMessage reques private void Receive(HttpRequestMessage message) { var msgText = message.Content.ReadAsStringAsync().Result; - // Setting "Sentry" as tag to prevent the UnityLogHandlerIntegration from capturing this message and - // adding it as a breadcrumb, which in turn multiplies it on following (intercepted) HTTP requests... - // Note: remove the prefix once setting breadcrumb log level is possible - https://github.com/getsentry/sentry-unity/issues/60 - Debug.unityLogger.Log(LogType.Log, "Sentry", $"{_name} TEST: Intercepted HTTP Request #{_requests.Count} = {msgText}"); + Debug.Log($"{_name} TEST: Intercepted HTTP Request #{_requests.Count} = {msgText}"); _requests.Enqueue(msgText); _requestReceived.Set(); } diff --git a/test/Scripts.Integration.Test/run-smoke-test.ps1 b/test/Scripts.Integration.Test/run-smoke-test.ps1 index 8c4423efa..c98268c06 100644 --- a/test/Scripts.Integration.Test/run-smoke-test.ps1 +++ b/test/Scripts.Integration.Test/run-smoke-test.ps1 @@ -105,7 +105,7 @@ function RunTest([string] $type) # Wait for the test to finish $timedOut = $null # reset any previously set timeout - $process | Wait-Process -Timeout 60 -ErrorAction SilentlyContinue -ErrorVariable timedOut + $process | Wait-Process -Timeout 180 -ErrorAction SilentlyContinue -ErrorVariable timedOut $appLog = "" if ("$AppDataDir" -ne "") diff --git a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs index 36252b96e..408ba83bd 100644 --- a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs @@ -1,93 +1,97 @@ -using System.Threading; +using System.Collections; using NUnit.Framework; using Sentry.Unity.Tests.Stubs; +using UnityEngine; +using UnityEngine.TestTools; namespace Sentry.Unity.Tests; public class ScreenshotEventProcessorTests { - private class Fixture + [Test] + public void Process_FirstCallInAFrame_StartsCoroutine() { - public SentryUnityOptions Options = new() { AttachScreenshot = true }; - public TestApplication TestApplication = new(); - - public ScreenshotEventProcessor GetSut() => new(Options, TestApplication); - } + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var screenshotProcessor = new ScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); - private Fixture _fixture = null!; + screenshotProcessor.Process(new SentryEvent()); - [SetUp] - public void SetUp() => _fixture = new Fixture(); + Assert.IsTrue(sentryMonoBehaviour.StartCoroutineCalled); + } - [TearDown] - public void TearDown() + [UnityTest] + public IEnumerator Process_ExecutesCoroutine_CapturesScreenshotAndCapturesAttachment() { - if (SentrySdk.IsEnabled) + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var screenshotProcessor = new ScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); + + var capturedEventId = SentryId.Empty; + SentryAttachment? capturedAttachment = null; + screenshotProcessor.AttachmentCaptureFunction = (eventId, attachment) => { - SentrySdk.Close(); - } - } + capturedEventId = eventId; + capturedAttachment = attachment; + }; - [Test] - public void Process_IsMainThread_AddsScreenshotToHint() - { - _fixture.TestApplication.IsEditor = false; - var sut = _fixture.GetSut(); - var sentryEvent = new SentryEvent(); - var hint = new SentryHint(); + // Replace WaitForEndOfFrame to return immediately + screenshotProcessor.WaitForEndOfFrameFunction = () => null!; + + var eventId = SentryId.Create(); + var sentryEvent = new SentryEvent(eventId: eventId); + + screenshotProcessor.Process(sentryEvent); - sut.Process(sentryEvent, hint); + // Wait for the coroutine to complete - need to wait for processing + yield return null; + yield return null; - Assert.AreEqual(1, hint.Attachments.Count); + Assert.IsTrue(sentryMonoBehaviour.StartCoroutineCalled); + Assert.AreEqual(eventId, capturedEventId); + Assert.NotNull(capturedAttachment); // Sanity check + Assert.AreEqual("screenshot.jpg", capturedAttachment!.FileName); + Assert.AreEqual("image/jpeg", capturedAttachment.ContentType); + Assert.AreEqual(AttachmentType.Default, capturedAttachment.Type); } - [Test] - public void Process_IsNonMainThread_DoesNotAddScreenshotToHint() + [UnityTest] + public IEnumerator Process_CalledMultipleTimesQuickly_OnlyExecutesScreenshotCaptureOnce() { - var sut = _fixture.GetSut(); - var sentryEvent = new SentryEvent(); - var hint = new SentryHint(); + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var screenshotProcessor = new ScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); - new Thread(() => + var screenshotCaptureCallCount = 0; + screenshotProcessor.ScreenshotCaptureFunction = _ => { - Thread.CurrentThread.IsBackground = true; - var stream = sut.Process(sentryEvent, hint); + screenshotCaptureCallCount++; + return [0]; + }; - Assert.AreEqual(0, hint.Attachments.Count); - }).Start(); - } + var attachmentCaptureCallCount = 0; + screenshotProcessor.AttachmentCaptureFunction = (_, _) => + { + attachmentCaptureCallCount++; + }; - [Test] - [TestCase(true)] - [TestCase(false)] - public void Process_BeforeCaptureScreenshotCallbackProvided_RespectsScreenshotCaptureDecision(bool captureScreenshot) - { - _fixture.TestApplication.IsEditor = false; - _fixture.Options.SetBeforeCaptureScreenshot(() => captureScreenshot); - var sut = _fixture.GetSut(); - var sentryEvent = new SentryEvent(); - var hint = new SentryHint(); + // Replace WaitForEndOfFrame to return immediately + screenshotProcessor.WaitForEndOfFrameFunction = () => null!; - sut.Process(sentryEvent, hint); + // Process multiple events quickly (before any coroutine can complete) + screenshotProcessor.Process(new SentryEvent()); + screenshotProcessor.Process(new SentryEvent()); + screenshotProcessor.Process(new SentryEvent()); - Assert.AreEqual(captureScreenshot ? 1 : 0, hint.Attachments.Count); + // Wait for the coroutine to complete - need to wait for processing + yield return null; + yield return null; + + Assert.AreEqual(1, screenshotCaptureCallCount); + Assert.AreEqual(1, attachmentCaptureCallCount); } - [Test] - [TestCase(true, 0)] - [TestCase(false, 1)] - public void Process_InEditorEnvironment_DoesNotCaptureScreenshot(bool isEditor, int expectedAttachmentCount) + private static TestSentryMonoBehaviour GetTestMonoBehaviour() { - // Arrange - _fixture.TestApplication.IsEditor = isEditor; - var sut = _fixture.GetSut(); - var sentryEvent = new SentryEvent(); - var hint = new SentryHint(); - - // Act - sut.Process(sentryEvent, hint); - - // Assert - Assert.AreEqual(expectedAttachmentCount, hint.Attachments.Count); + var gameObject = new GameObject("ScreenshotProcessorTest"); + var behaviour = gameObject.AddComponent(); + return behaviour; } } diff --git a/test/Sentry.Unity.Tests/SentryMonoBehaviourTests.cs b/test/Sentry.Unity.Tests/SentryMonoBehaviourTests.cs index 268bb2fcb..8b7e6ed45 100644 --- a/test/Sentry.Unity.Tests/SentryMonoBehaviourTests.cs +++ b/test/Sentry.Unity.Tests/SentryMonoBehaviourTests.cs @@ -1,6 +1,8 @@ +using System.Collections; using NUnit.Framework; using Sentry.Unity.Tests.Stubs; using UnityEngine; +using UnityEngine.TestTools; namespace Sentry.Unity.Tests; @@ -78,4 +80,5 @@ public void UpdatePauseStatus_ResumedTwice_ApplicationResumingInvokedOnlyOnce() Assert.AreEqual(1, counter); } + } diff --git a/test/Sentry.Unity.Tests/Stubs/SentryTestMonoBehaviour.cs b/test/Sentry.Unity.Tests/Stubs/SentryTestMonoBehaviour.cs new file mode 100644 index 000000000..12ffb5add --- /dev/null +++ b/test/Sentry.Unity.Tests/Stubs/SentryTestMonoBehaviour.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace Sentry.Unity.Tests.Stubs; + +internal class TestSentryMonoBehaviour : MonoBehaviour, ISentryMonoBehaviour +{ + public event System.Action? ApplicationResuming; + public void ResumeApplication() => ApplicationResuming?.Invoke(); + + public bool StartCoroutineCalled { get; private set; } + + public new Coroutine StartCoroutine(IEnumerator routine) + { + StartCoroutineCalled = true; + return base.StartCoroutine(routine); + } +} diff --git a/test/Sentry.Unity.Tests/Stubs/TestHub.cs b/test/Sentry.Unity.Tests/Stubs/TestHub.cs index e9d7b2d86..a4d9a0e6c 100644 --- a/test/Sentry.Unity.Tests/Stubs/TestHub.cs +++ b/test/Sentry.Unity.Tests/Stubs/TestHub.cs @@ -187,4 +187,9 @@ public void CaptureFeedback(SentryFeedback feedback, Action configureScop { throw new NotImplementedException(); } + + public bool CaptureAttachment(SentryId eventId, SentryAttachment attachment) + { + throw new NotImplementedException(); + } } diff --git a/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs index 91189d44e..470918e21 100644 --- a/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs @@ -93,11 +93,4 @@ public void TraceGeneration_OnActiveSceneChange_GeneratesNewTrace() Assert.AreNotEqual(initialPropagationContext, scope.PropagationContext); } - - internal class TestSentryMonoBehaviour : ISentryMonoBehaviour - { - public event Action? ApplicationResuming; - - public void ResumeApplication() => ApplicationResuming?.Invoke(); - } }