diff --git a/CHANGELOG.md b/CHANGELOG.md index efe46a88a..e4651686b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ **Migration**: Update your `using` directives from `using Sentry;` to `using Sentry.Unity;`. IDEs like Rider can automatically import the missing references. In some cases, you may need both `using Sentry.Unity;` (for the static API) and `using Sentry;` (for types like `SentryId`). No changes are required to your actual SDK method calls (e.g., `SentrySdk.CaptureException()` - remains the same). ([#2227](https://github.com/getsentry/sentry-unity/pull/2227)) + remains the same). ([#2227](https://github.com/getsentry/sentry-unity/pull/2227), [#2239](https://github.com/getsentry/sentry-unity/pull/2239)) ### Features diff --git a/src/Sentry.Unity/Il2CppEventProcessor.cs b/src/Sentry.Unity/Il2CppEventProcessor.cs index 612dcbc93..16e6e3684 100644 --- a/src/Sentry.Unity/Il2CppEventProcessor.cs +++ b/src/Sentry.Unity/Il2CppEventProcessor.cs @@ -17,12 +17,15 @@ internal class UnityIl2CppEventExceptionProcessor : ISentryEventExceptionProcess private static ISentryUnityInfo UnityInfo = null!; // private static will be initialized in the constructor private readonly Il2CppMethods _il2CppMethods; - public UnityIl2CppEventExceptionProcessor(SentryUnityOptions options, ISentryUnityInfo unityInfo) + public UnityIl2CppEventExceptionProcessor(SentryUnityOptions options) { Options = options; - UnityInfo = unityInfo; - _il2CppMethods = unityInfo.Il2CppMethods ?? throw new ArgumentNullException(nameof(unityInfo.Il2CppMethods), - "Unity IL2CPP methods are not available."); + + // We're throwing here but this should never happen. We're validating UnityInfo before adding the processor. + UnityInfo = SentryPlatformServices.UnityInfo + ?? throw new ArgumentNullException(nameof(SentryPlatformServices.UnityInfo), "UnityInfo is null"); + _il2CppMethods = UnityInfo.Il2CppMethods + ?? throw new ArgumentNullException(nameof(UnityInfo.Il2CppMethods), "Unity IL2CPP methods are not available."); Options.SdkIntegrationNames.Add("IL2CPPLineNumbers"); } diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 2b401242d..410589eb3 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -136,7 +136,7 @@ internal SentryUnityOptions ToSentryUnityOptions(bool isBuilding, ISentryUnityIn { application ??= ApplicationAdapter.Instance; - var options = new SentryUnityOptions(isBuilding, application, unityInfo) + var options = new SentryUnityOptions(isBuilding, application, unityInfo, SentryMonoBehaviour.Instance) { Enabled = Enabled, Dsn = Dsn, @@ -186,6 +186,12 @@ internal SentryUnityOptions ToSentryUnityOptions(bool isBuilding, ISentryUnityIn PerformanceAutoInstrumentationEnabled = AutoAwakeTraces, }; + // By default, the cacheDirectoryPath gets set on known platforms. The option is enabled by default + if (!EnableOfflineCaching) + { + options.CacheDirectoryPath = null; + } + if (!string.IsNullOrWhiteSpace(ReleaseOverride)) { options.Release = ReleaseOverride; @@ -227,23 +233,11 @@ internal SentryUnityOptions ToSentryUnityOptions(bool isBuilding, ISentryUnityIn // Without setting up here we might miss out on logs between option-loading (now) and Init - i.e. native configuration options.SetupUnityLogging(); - if (options.AttachViewHierarchy) - { - options.AddEventProcessor(new ViewHierarchyEventProcessor(options)); - } - if (options.AttachScreenshot) - { - options.AddEventProcessor(new ScreenshotEventProcessor(options)); - } - - if (!application.IsEditor && options.Il2CppLineNumberSupportEnabled && unityInfo is not null) - { - options.AddIl2CppExceptionProcessor(unityInfo); - } - - HandlePlatformRestrictedOptions(options, unityInfo, application); + // ExceptionFilters are added by default to the options. HandleExceptionFilter(options); + // The AnrDetectionIntegration is added by default. Since it is a ScriptableUnityOptions-only property we have to + // remove the integration when creating the options through here if (!AnrDetectionEnabled) { options.DisableAnrIntegration(); @@ -252,41 +246,6 @@ internal SentryUnityOptions ToSentryUnityOptions(bool isBuilding, ISentryUnityIn return options; } - internal void HandlePlatformRestrictedOptions(SentryUnityOptions options, ISentryUnityInfo? unityInfo, IApplication application) - { - if (unityInfo?.IsKnownPlatform() == false) - { - options.DisableFileWrite = true; - - // This is only provided on a best-effort basis for other than the explicitly supported platforms. - if (options.BackgroundWorker is null) - { - options.DiagnosticLogger?.LogDebug("Platform support for background thread execution is unknown: using WebBackgroundWorker."); - options.BackgroundWorker = new WebBackgroundWorker(options, SentryMonoBehaviour.Instance); - } - - // Disable offline caching regardless whether it was enabled or not. - options.CacheDirectoryPath = null; - if (EnableOfflineCaching) - { - options.DiagnosticLogger?.LogDebug("Platform support for offline caching is unknown: disabling."); - } - - // Requires file access, see https://github.com/getsentry/sentry-unity/issues/290#issuecomment-1163608988 - if (options.AutoSessionTracking) - { - options.DiagnosticLogger?.LogDebug("Platform support for automatic session tracking is unknown: disabling."); - options.AutoSessionTracking = false; - } - - return; - } - - // Only assign the cache directory path if we're on a "known" platform. Accessing `Application.persistentDataPath` - // implicitly creates a directory and leads to crashes i.e. on the Switch. - options.CacheDirectoryPath = EnableOfflineCaching ? application.PersistentDataPath : null; - } - private void HandleExceptionFilter(SentryUnityOptions options) { if (!options.FilterBadGatewayExceptions) diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index ed1184db2..143dbb5e4 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using Sentry.Unity.Integrations; using Sentry.Extensibility; +using Sentry.Unity.NativeUtils; using UnityEngine; using CompressionLevel = System.IO.Compression.CompressionLevel; @@ -294,39 +295,38 @@ internal string? DefaultUserId internal List SdkIntegrationNames { get; set; } = new(); - public SentryUnityOptions() : this(false, ApplicationAdapter.Instance) { } - - internal SentryUnityOptions(bool isBuilding, IApplication application, ISentryUnityInfo? unityInfo = null) : - this(SentryMonoBehaviour.Instance, application, isBuilding, unityInfo) + public SentryUnityOptions() : + this(false, ApplicationAdapter.Instance, SentryPlatformServices.UnityInfo, SentryMonoBehaviour.Instance) { } // For testing - internal SentryUnityOptions(SentryMonoBehaviour behaviour, IApplication application, bool isBuilding, ISentryUnityInfo? unityInfo = null) + internal SentryUnityOptions(bool isBuilding, IApplication application, ISentryUnityInfo? unityInfo, + SentryMonoBehaviour behaviour) { // IL2CPP doesn't support Process.GetCurrentProcess().StartupTime DetectStartupTime = StartupTimeDetectionMode.Fast; - this.AddInAppExclude("UnityEngine"); - this.AddInAppExclude("UnityEditor"); + AddInAppExclude("UnityEngine"); + AddInAppExclude("UnityEditor"); var processor = new UnityEventProcessor(this); - this.AddEventProcessor(processor); - this.AddTransactionProcessor(processor); - this.AddExceptionProcessor(new UnityExceptionProcessor()); - - this.AddIntegration(new UnityLogHandlerIntegration(this)); - this.AddIntegration(new UnityApplicationLoggingIntegration()); - this.AddIntegration(new StartupTracingIntegration()); - this.AddIntegration(new AnrIntegration(behaviour)); - this.AddIntegration(new UnityScopeIntegration(application, unityInfo)); - this.AddIntegration(new UnityBeforeSceneLoadIntegration()); - this.AddIntegration(new SceneManagerIntegration()); - this.AddIntegration(new SceneManagerTracingIntegration()); - this.AddIntegration(new SessionIntegration(behaviour)); - this.AddIntegration(new TraceGenerationIntegration(behaviour)); - - this.AddExceptionFilter(new UnityBadGatewayExceptionFilter()); - this.AddExceptionFilter(new UnityWebExceptionFilter()); - this.AddExceptionFilter(new UnitySocketExceptionFilter()); + AddEventProcessor(processor); + AddTransactionProcessor(processor); + AddExceptionProcessor(new UnityExceptionProcessor()); + + AddIntegration(new UnityLogHandlerIntegration(this)); + AddIntegration(new UnityApplicationLoggingIntegration()); + AddIntegration(new StartupTracingIntegration()); + AddIntegration(new AnrIntegration(behaviour)); + AddIntegration(new UnityScopeIntegration(application, unityInfo)); + AddIntegration(new UnityBeforeSceneLoadIntegration()); + AddIntegration(new SceneManagerIntegration()); + AddIntegration(new SceneManagerTracingIntegration()); + AddIntegration(new SessionIntegration(behaviour)); + AddIntegration(new TraceGenerationIntegration(behaviour)); + + AddExceptionFilter(new UnityBadGatewayExceptionFilter()); + AddExceptionFilter(new UnityWebExceptionFilter()); + AddExceptionFilter(new UnitySocketExceptionFilter()); IsGlobalModeEnabled = true; @@ -363,6 +363,13 @@ internal SentryUnityOptions(SentryMonoBehaviour behaviour, IApplication applicat { LogType.Error, true}, { LogType.Exception, true}, }; + + // Only assign the cache directory path if we're on a "known" platform. Accessing `Application.persistentDataPath` + // implicitly creates a directory and leads to crashes i.e. on the Switch. + if (unityInfo?.IsKnownPlatform() ?? false) + { + CacheDirectoryPath = application.PersistentDataPath; + } } public override string ToString() diff --git a/src/Sentry.Unity/SentryUnityOptionsExtensions.cs b/src/Sentry.Unity/SentryUnityOptionsExtensions.cs index 773e45391..6793e0116 100644 --- a/src/Sentry.Unity/SentryUnityOptionsExtensions.cs +++ b/src/Sentry.Unity/SentryUnityOptionsExtensions.cs @@ -63,18 +63,6 @@ internal static void SetupUnityLogging(this SentryUnityOptions options) } } - internal static void AddIl2CppExceptionProcessor(this SentryUnityOptions options, ISentryUnityInfo unityInfo) - { - if (unityInfo.Il2CppMethods is not null) - { - options.AddExceptionProcessor(new UnityIl2CppEventExceptionProcessor(options, unityInfo)); - } - else - { - options.DiagnosticLogger?.LogWarning("Failed to find required IL2CPP methods - Skipping line number support"); - } - } - /// /// Disables the capture of errors through . /// diff --git a/src/Sentry.Unity/SentryUnitySdk.cs b/src/Sentry.Unity/SentryUnitySdk.cs index dcaed310e..39ed06e57 100644 --- a/src/Sentry.Unity/SentryUnitySdk.cs +++ b/src/Sentry.Unity/SentryUnitySdk.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Sentry.Extensibility; using Sentry.Unity.Integrations; +using Sentry.Unity.NativeUtils; using UnityEngine; namespace Sentry.Unity; @@ -30,24 +31,11 @@ private SentryUnitySdk(SentryUnityOptions options) MainThreadData.CollectData(); - // On Standalone, we disable cache dir in case multiple app instances run over the same path. - // Note: we cannot use a named Mutex, because Unit doesn't support it. Instead, we create a file with `FileShare.None`. - // https://forum.unity.com/threads/unsupported-internal-call-for-il2cpp-mutex-createmutex_internal-named-mutexes-are-not-supported.387334/ - if (ApplicationAdapter.Instance.Platform is RuntimePlatform.WindowsPlayer && options.CacheDirectoryPath is not null) - { - try - { - unitySdk._lockFile = new FileStream(Path.Combine(options.CacheDirectoryPath, "sentry-unity.lock"), FileMode.OpenOrCreate, - FileAccess.ReadWrite, FileShare.None); - } - catch (Exception ex) - { - options.DiagnosticLogger?.LogWarning("An exception was thrown while trying to " + - "acquire a lockfile on the config directory: .NET event cache will be disabled.", ex); - options.CacheDirectoryPath = null; - options.AutoSessionTracking = false; - } - } + // Some integrations are controlled through a flag and opt-in. Adding these integrations late so we have the + // same behaviour whether we're self or manually initializing + HandleLateIntegrations(options); + HandlePlatformRestrictedOptions(options, SentryPlatformServices.UnityInfo); + HandleWindowsPlayer(unitySdk, options); unitySdk._dotnetSdk = Sentry.SentrySdk.Init(options); @@ -137,4 +125,77 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad Sentry.SentrySdk.CurrentHub.CaptureFeedback(message, email, name, hint: hint); } + + internal static void HandleWindowsPlayer(SentryUnitySdk unitySdk, SentryUnityOptions options) + { + // On Windows-Standalone, we disable cache dir in case multiple app instances run over the same path. + // Note: we cannot use a named Mutex, because Unity doesn't support it. Instead, we create a file with `FileShare.None`. + // https://forum.unity.com/threads/unsupported-internal-call-for-il2cpp-mutex-createmutex_internal-named-mutexes-are-not-supported.387334/ + if (ApplicationAdapter.Instance.Platform is not RuntimePlatform.WindowsPlayer || + options.CacheDirectoryPath is null) + { + return; + } + + try + { + unitySdk._lockFile = new FileStream(Path.Combine(options.CacheDirectoryPath, "sentry-unity.lock"), FileMode.OpenOrCreate, + FileAccess.ReadWrite, FileShare.None); + } + catch (Exception ex) + { + options.DiagnosticLogger?.LogWarning("An exception was thrown while trying to " + + "acquire a lockfile on the config directory: .NET event cache will be disabled.", ex); + options.CacheDirectoryPath = null; + options.AutoSessionTracking = false; + } + } + + internal static void HandleLateIntegrations(SentryUnityOptions options) + { + if (options.AttachViewHierarchy) + { + options.AddEventProcessor(new ViewHierarchyEventProcessor(options)); + } + if (options.AttachScreenshot) + { + options.AddEventProcessor(new ScreenshotEventProcessor(options)); + } + + if (!ApplicationAdapter.Instance.IsEditor && + (SentryPlatformServices.UnityInfo?.IL2CPP ?? false) && + options.Il2CppLineNumberSupportEnabled) + { + if (SentryPlatformServices.UnityInfo.Il2CppMethods is not null) + { + options.AddExceptionProcessor(new UnityIl2CppEventExceptionProcessor(options)); + } + else + { + options.DiagnosticLogger?.LogWarning("Failed to find required IL2CPP methods - Skipping line number support"); + } + } + } + + internal static void HandlePlatformRestrictedOptions(SentryUnityOptions options, ISentryUnityInfo? unityInfo) + { + if (unityInfo?.IsKnownPlatform() == false) + { + options.DisableFileWrite = true; + + // Requires file access, see https://github.com/getsentry/sentry-unity/issues/290#issuecomment-1163608988 + if (options.AutoSessionTracking) + { + options.DiagnosticLogger?.LogDebug("Platform support for automatic session tracking is unknown: disabling."); + options.AutoSessionTracking = false; + } + + // This is only provided on a best-effort basis for other than the explicitly supported platforms. + if (options.BackgroundWorker is null) + { + options.DiagnosticLogger?.LogDebug("Platform support for background thread execution is unknown: using WebBackgroundWorker."); + options.BackgroundWorker = new WebBackgroundWorker(options, SentryMonoBehaviour.Instance); + } + } + } } diff --git a/test/Sentry.Unity.Tests/ContextWriterTests.cs b/test/Sentry.Unity.Tests/ContextWriterTests.cs index b432c5ea0..e72aedd50 100644 --- a/test/Sentry.Unity.Tests/ContextWriterTests.cs +++ b/test/Sentry.Unity.Tests/ContextWriterTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using NUnit.Framework; +using Sentry.Unity.NativeUtils; using Sentry.Unity.Tests.SharedClasses; using Sentry.Unity.Tests.Stubs; using UnityEngine; @@ -66,7 +67,7 @@ public void Arguments() }; var context = new MockContextWriter(); - var options = new SentryUnityOptions(_sentryMonoBehaviour, _testApplication, false) + var options = new SentryUnityOptions(false, _testApplication, SentryPlatformServices.UnityInfo, _sentryMonoBehaviour) { Dsn = "http://publickey@localhost/12345", Enabled = true, diff --git a/test/Sentry.Unity.Tests/ScriptableSentryUnityOptionsTests.cs b/test/Sentry.Unity.Tests/ScriptableSentryUnityOptionsTests.cs index 313755550..7ec68bba4 100644 --- a/test/Sentry.Unity.Tests/ScriptableSentryUnityOptionsTests.cs +++ b/test/Sentry.Unity.Tests/ScriptableSentryUnityOptionsTests.cs @@ -127,18 +127,6 @@ public void ToSentryUnityOptions_HasOptionsConfiguration_GetsCalled(bool isBuild Assert.AreEqual(optionsConfiguration.GotCalled, !isBuilding); } - [Test] - public void ToSentryUnityOptions_UnknownPlatforms_DoesNotAccessDisk() - { - var scriptableOptions = ScriptableObject.CreateInstance(); - _fixture.UnityInfo = new TestUnityInfo(false); - - var options = scriptableOptions.ToSentryUnityOptions(false, _fixture.UnityInfo, _fixture.Application); - - Assert.IsNull(options.CacheDirectoryPath); - Assert.IsFalse(options.AutoSessionTracking); - } - [Test] public void ToSentryUnityOptions_WebExceptionFilterAdded() { @@ -178,42 +166,6 @@ public void ToSentryUnityOptions_UnityBadGatewayExceptionFilterAdded() Assert.True(filters.OfType().Any()); } - [Test] - public void HandlePlatformRestrictedOptions_UnknownPlatform_SetsRestrictedOptions() - { - _fixture.UnityInfo = new TestUnityInfo(false); - - var scriptableOptions = ScriptableObject.CreateInstance(); - scriptableOptions.EnableOfflineCaching = true; - - var options = new SentryUnityOptions - { - DisableFileWrite = false, - CacheDirectoryPath = "some/path", - AutoSessionTracking = true - }; - - scriptableOptions.HandlePlatformRestrictedOptions(options, _fixture.UnityInfo, _fixture.Application); - - Assert.IsTrue(options.DisableFileWrite); - Assert.IsNull(options.CacheDirectoryPath); - Assert.IsFalse(options.AutoSessionTracking); - Assert.IsTrue(options.BackgroundWorker is WebBackgroundWorker); - } - - [Test] - public void HandlePlatformRestrictedOptions_KnownPlatform_SetsRestrictedOptions() - { - var scriptableOptions = ScriptableObject.CreateInstance(); - scriptableOptions.EnableOfflineCaching = true; - - var options = new SentryUnityOptions(); - - scriptableOptions.HandlePlatformRestrictedOptions(options, _fixture.UnityInfo, _fixture.Application); - - Assert.AreEqual(options.CacheDirectoryPath, _fixture.Application.PersistentDataPath); - } - public static void AssertOptions(SentryUnityOptions expected, SentryUnityOptions actual) { Assert.AreEqual(expected.Enabled, actual.Enabled); diff --git a/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs b/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs index 943f45ac3..d094b36a4 100644 --- a/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs +++ b/test/Sentry.Unity.Tests/SentryUnityOptionsTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Sentry.Unity.NativeUtils; using Sentry.Unity.Tests.Stubs; namespace Sentry.Unity.Tests; @@ -7,6 +8,7 @@ public sealed class SentryUnityOptionsTests { class Fixture { + public TestUnityInfo UnityInfo { get; set; } = new(); public TestApplication Application { get; set; } = new( productName: "TestApplication", version: "0.1.0", @@ -14,7 +16,7 @@ class Fixture persistentDataPath: "test/persistent/data/path"); public bool IsBuilding { get; set; } - public SentryUnityOptions GetSut() => new SentryUnityOptions(IsBuilding, Application); + public SentryUnityOptions GetSut() => new(IsBuilding, Application, UnityInfo, SentryMonoBehaviour.Instance); } [SetUp] @@ -37,7 +39,17 @@ public void Ctor_Environment_IsNull(bool isEditor, bool isBuilding, string expec } [Test] - public void Ctor_CacheDirectoryPath_IsNull() => Assert.IsNull(_fixture.GetSut().CacheDirectoryPath); + [TestCase(true, "some/path", "some/path")] + [TestCase(false, "some/path", null)] + public void Ctor_IfPlatformIsKnown_SetsCacheDirectoryPath(bool isKnownPlatform, string applicationDataPath, string? expectedCacheDirectoryPath) + { + _fixture.UnityInfo = new TestUnityInfo(isKnownPlatform: isKnownPlatform); + _fixture.Application.PersistentDataPath = applicationDataPath; + + var sut = _fixture.GetSut(); + + Assert.AreEqual(sut.CacheDirectoryPath, expectedCacheDirectoryPath); + } [Test] public void Ctor_IsGlobalModeEnabled_IsTrue() => Assert.IsTrue(_fixture.GetSut().IsGlobalModeEnabled); diff --git a/test/Sentry.Unity.Tests/SentryUnityTests.cs b/test/Sentry.Unity.Tests/SentryUnityTests.cs index e3326cad6..e14e932d5 100644 --- a/test/Sentry.Unity.Tests/SentryUnityTests.cs +++ b/test/Sentry.Unity.Tests/SentryUnityTests.cs @@ -6,6 +6,7 @@ using Debug = UnityEngine.Debug; using Sentry.Extensibility; using Sentry.Unity.Tests.SharedClasses; +using Sentry.Unity.Tests.Stubs; namespace Sentry.Unity.Tests; @@ -173,4 +174,21 @@ public void GetLastRunState_WithNullDelegate_ReturnsUnknown() // Assert Assert.AreEqual(SentrySdk.CrashedLastRun.Unknown, result); } + + [Test] + public void HandlePlatformRestrictedOptions_UnknownPlatform_SetsRestrictedOptions() + { + var unityInfo = new TestUnityInfo(false); + var options = new SentryUnityOptions + { + DisableFileWrite = false, + AutoSessionTracking = true + }; + + SentryUnitySdk.HandlePlatformRestrictedOptions(options, unityInfo); + + Assert.IsTrue(options.DisableFileWrite); + Assert.IsFalse(options.AutoSessionTracking); + Assert.IsTrue(options.BackgroundWorker is WebBackgroundWorker); + } } diff --git a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs index 2085e1682..a2e5c93f8 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -113,7 +113,7 @@ public void SentrySdkCaptureEvent(bool captureOnUiThread) RenderingThreadingMode = new Lazy(() => "MultiThreaded"), StartTime = new(() => DateTimeOffset.UtcNow), }; - var options = new SentryUnityOptions(_sentryMonoBehaviour, _testApplication, false, new TestUnityInfo { IL2CPP = true }) + var options = new SentryUnityOptions(false, _testApplication, new TestUnityInfo { IL2CPP = true }, _sentryMonoBehaviour) { Dsn = "https://b8fd848b31444e80aa102e96d2a6a648@o510466.ingest.sentry.io/5606182", Enabled = true, diff --git a/test/SharedClasses/TestApplication.cs b/test/SharedClasses/TestApplication.cs index d41a46826..c8551297d 100644 --- a/test/SharedClasses/TestApplication.cs +++ b/test/SharedClasses/TestApplication.cs @@ -32,7 +32,7 @@ public TestApplication( public string Version { get; } public string BuildGUID { get; } public string UnityVersion { get; set; } - public string PersistentDataPath { get; } + public string PersistentDataPath { get; set; } public RuntimePlatform Platform { get; set; } private void OnQuitting() => Quitting?.Invoke();