diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index 81ac5c721..687d94635 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -31,7 +31,9 @@ public static bool Init(SentryUnityOptions options) UseLibC = Application.platform is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer or RuntimePlatform.PS5 or RuntimePlatform.Switch; - IsWindows = Application.platform is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer; + IsWindows = Application.platform + is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer + or RuntimePlatform.GameCoreXboxSeries or RuntimePlatform.GameCoreXboxOne; var cOptions = sentry_options_new(); diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index e547dc59a..d4c3048c9 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -3,14 +3,16 @@ # Integration tests for Sentry Unity SDK # # Environment variables: -# SENTRY_TEST_PLATFORM: target platform (Android, Desktop, iOS, WebGL) -# SENTRY_TEST_DSN: test DSN +# SENTRY_TEST_PLATFORM: target platform (Android, Desktop, iOS, WebGL, Xbox) +# SENTRY_DSN: test DSN # SENTRY_AUTH_TOKEN: authentication token for Sentry API # -# SENTRY_TEST_APP: path to the test app (APK, executable, .app bundle, or WebGL build directory) +# SENTRY_TEST_APP: path to the test app (APK, executable, .app bundle, WebGL build directory, +# or Xbox packaged build directory containing a .xvc) # # Platform-specific environment variables: # iOS: SENTRY_IOS_VERSION - iOS simulator version (e.g. "17.0" or "latest") +# Xbox: XBCONNECT_TARGET - Xbox devkit IP address Set-StrictMode -Version latest $ErrorActionPreference = "Stop" @@ -30,9 +32,45 @@ BeforeAll { "Android" { return @("-e", "test", $Action) } "Desktop" { return @("--test", $Action, "-logFile", "-") } "iOS" { return @("--test", $Action) } + "Xbox" { return @("--test", $Action) } } } + # Retrieve the integration test log file from Xbox and attach it to the run result. + # The Unity app writes to D:\Logs\UnityIntegrationTest.log on Xbox (UNITY_GAMECORE). + function Get-XboxLogOutput { + param( + [Parameter(Mandatory=$true)] + $RunResult, + + [Parameter(Mandatory=$true)] + [string]$Action + ) + + $logFileName = "UnityIntegrationTest.log" + $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" + New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null + + try { + Copy-DeviceItem -DevicePath "D:\Logs\$logFileName" -Destination $logLocalDir + } catch { + throw "Failed to retrieve Xbox log file (D:\Logs\$logFileName): $_" + } + + $localFile = Join-Path $logLocalDir $logFileName + $logContent = Get-Content $localFile -ErrorAction SilentlyContinue + if (-not $logContent -or $logContent.Count -eq 0) { + $localFiles = Get-ChildItem -Path $logLocalDir -ErrorAction SilentlyContinue + $localContents = if ($localFiles) { ($localFiles | ForEach-Object { $_.Name }) -join ", " } else { "(empty — D:\Logs\ likely did not exist on device)" } + throw "Xbox log file was empty or missing (D:\Logs\$logFileName). Copied directory contains: $localContents" + } + + Write-Host "Retrieved log file from Xbox ($($logContent.Count) lines)" -ForegroundColor Green + + $RunResult.Output = $logContent + return $RunResult + } + # Run a WebGL test action via headless Chrome function Invoke-WebGLTestAction { param ( @@ -110,6 +148,12 @@ BeforeAll { $appArgs = Get-AppArguments -Action $Action $runResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $appArgs + # On Xbox, console output is not available in non-development builds. + # Retrieve the log file the app writes directly to disk. + if ($script:Platform -eq "Xbox") { + $runResult = Get-XboxLogOutput -RunResult $runResult -Action $Action + } + # Save result to JSON file $runResult | ConvertTo-Json -Depth 5 | Out-File -FilePath (Get-OutputFilePath "${Action}-result.json") @@ -120,6 +164,10 @@ BeforeAll { $sendArgs = Get-AppArguments -Action "crash-send" $sendResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $sendArgs + if ($script:Platform -eq "Xbox") { + $sendResult = Get-XboxLogOutput -RunResult $sendResult -Action "crash-send" + } + # Save crash-send result to JSON for debugging $sendResult | ConvertTo-Json -Depth 5 | Out-File -FilePath (Get-OutputFilePath "crash-send-result.json") @@ -142,12 +190,12 @@ BeforeAll { $script:Platform = $env:SENTRY_TEST_PLATFORM if ([string]::IsNullOrEmpty($script:Platform)) { - throw "SENTRY_TEST_PLATFORM environment variable is not set. Expected: Android, Desktop, iOS, or WebGL" + throw "SENTRY_TEST_PLATFORM environment variable is not set. Expected: Android, Desktop, iOS, WebGL, or Xbox" } # Validate common environment - if ([string]::IsNullOrEmpty($env:SENTRY_TEST_DSN)) { - throw "SENTRY_TEST_DSN environment variable is not set." + if ([string]::IsNullOrEmpty($env:SENTRY_DSN)) { + throw "SENTRY_DSN environment variable is not set." } if ([string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { throw "SENTRY_AUTH_TOKEN environment variable is not set." @@ -195,10 +243,25 @@ BeforeAll { Connect-Device -Platform "iOSSimulator" -Target $target Install-DeviceApp -Path $env:SENTRY_TEST_APP } + "Xbox" { + if ([string]::IsNullOrEmpty($env:XBCONNECT_TARGET)) { + throw "XBCONNECT_TARGET environment variable is not set." + } + + Connect-Device -Platform "Xbox" -Target $env:XBCONNECT_TARGET + + $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" | Select-Object -First 1 + if (-not $xvcFile) { + throw "No .xvc found in SENTRY_TEST_APP: $env:SENTRY_TEST_APP" + } + Install-DeviceApp -Path $xvcFile.FullName + $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP + Write-Host "Using AUMID: $($script:ExecutablePath)" + } "WebGL" { } default { - throw "Unknown platform: $($script:Platform). Expected: Android, Desktop, iOS, or WebGL" + throw "Unknown platform: $($script:Platform). Expected: Android, Desktop, iOS, WebGL, or Xbox" } } @@ -209,7 +272,7 @@ BeforeAll { # Initialize test parameters $script:TestSetup = [PSCustomObject]@{ Platform = $script:Platform - Dsn = $env:SENTRY_TEST_DSN + Dsn = $env:SENTRY_DSN AuthToken = $env:SENTRY_AUTH_TOKEN } diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 1e776efab..02a89335f 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -21,6 +21,7 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, // Make sure the configuration is right. EditorUserBuildSettings.selectedBuildTargetGroup = group; + EditorUserBuildSettings.development = false; EditorUserBuildSettings.allowDebugging = false; PlayerSettings.SetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(group), ScriptingImplementation.IL2CPP); // Making sure that the app keeps on running in the background. Linux CI is very unhappy with coroutines otherwise. @@ -113,16 +114,21 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, } } + [MenuItem("Tools/Builder/Windows")] public static void BuildWindowsIl2CPPPlayer() { Debug.Log("Builder: Building Windows IL2CPP Player"); BuildIl2CPPPlayer(BuildTarget.StandaloneWindows64, BuildTargetGroup.Standalone, BuildOptions.StrictMode); } + + [MenuItem("Tools/Builder/macOS")] public static void BuildMacIl2CPPPlayer() { Debug.Log("Builder: Building macOS IL2CPP Player"); BuildIl2CPPPlayer(BuildTarget.StandaloneOSX, BuildTargetGroup.Standalone, BuildOptions.StrictMode); } + + [MenuItem("Tools/Builder/Linux")] public static void BuildLinuxIl2CPPPlayer() { Debug.Log("Builder: Building Linux IL2CPP Player"); @@ -132,6 +138,8 @@ public static void BuildLinuxIl2CPPPlayer() PlayerSettings.graphicsJobs = false; BuildIl2CPPPlayer(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone, BuildOptions.StrictMode); } + + [MenuItem("Tools/Builder/Android")] public static void BuildAndroidIl2CPPPlayer() { Debug.Log("Builder: Building Android IL2CPP Player"); @@ -156,17 +164,22 @@ public static void BuildAndroidIl2CPPPlayer() BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/Android Project")] public static void BuildAndroidIl2CPPProject() { Debug.Log("Builder: Building Android IL2CPP Project"); EditorUserBuildSettings.exportAsGoogleAndroidProject = true; BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android, BuildOptions.AcceptExternalModificationsToPlayer); } + + [MenuItem("Tools/Builder/iOS")] public static void BuildIOSProject() { Debug.Log("Builder: Building iOS Project"); BuildIl2CPPPlayer(BuildTarget.iOS, BuildTargetGroup.iOS, BuildOptions.StrictMode); } + + [MenuItem("Tools/Builder/WebGL")] public static void BuildWebGLPlayer() { Debug.Log("Builder: Building WebGL Player"); @@ -174,36 +187,151 @@ public static void BuildWebGLPlayer() BuildIl2CPPPlayer(BuildTarget.WebGL, BuildTargetGroup.WebGL, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/Switch")] public static void BuildSwitchIL2CPPPlayer() { Debug.Log("Builder: Building Switch IL2CPP Player"); + SetSwitchCreateNspRomFile(); BuildIl2CPPPlayer(BuildTarget.Switch, BuildTargetGroup.Switch, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/Xbox Series X|S")] public static void BuildXSXIL2CPPPlayer() { Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player"); + SetXboxSubtargetToMaster(); BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/Xbox One")] public static void BuildXB1IL2CPPPlayer() { Debug.Log("Builder: Building Xbox One IL2CPP Player"); + SetXboxSubtargetToMaster(); BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/PS5")] public static void BuildPS5IL2CPPPlayer() { Debug.Log("Builder: Building PS5 IL2CPP Player"); + SetPS5BuildTypeToPackage(); BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode); } + private static void SetXboxSubtargetToMaster() + { + // The actual editor API to set this has been deprecated: https://docs.unity3d.com/6000.3/Documentation/ScriptReference/XboxBuildSubtarget.html + // Modifying the build profiles and build setting assets on disk does not work. Some of the properties are + // stored inside a binary. Instead we're setting the properties via reflection and then saving the asset. + var buildProfileType = Type.GetType("UnityEditor.Build.Profile.BuildProfile, UnityEditor.CoreModule"); + if (buildProfileType == null) + { + return; + } + + foreach (var profile in Resources.FindObjectsOfTypeAll(buildProfileType)) + { + // BuildTarget.GameCoreXboxSeries = 42, BuildTarget.GameCoreXboxOne = 43. + var buildTarget = new SerializedObject(profile).FindProperty("m_BuildTarget")?.intValue ?? -1; + if (buildTarget != 42 && buildTarget != 43) + continue; + + var platformSettings = buildProfileType + .GetProperty("platformBuildProfile", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(profile); + var settingsData = platformSettings?.GetType() + .GetField("m_settingsData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(platformSettings); + + GetFieldInHierarchy(settingsData?.GetType(), "buildSubtarget")?.SetValue(settingsData, 1); // 1 = Master + GetFieldInHierarchy(platformSettings?.GetType(), "m_Development")?.SetValue(platformSettings, false); + GetFieldInHierarchy(settingsData?.GetType(), "deploymentMethod")?.SetValue(settingsData, 2); // 2 = Package + + EditorUtility.SetDirty(profile); + Debug.Log($"Builder: Xbox Build Profile (BuildTarget {buildTarget}) set to Master, deploy method set to Package"); + } + + AssetDatabase.SaveAssets(); + } + + private static void SetPS5BuildTypeToPackage() + { + var buildProfileType = Type.GetType("UnityEditor.Build.Profile.BuildProfile, UnityEditor.CoreModule"); + if (buildProfileType == null) + { + return; + } + + foreach (var profile in Resources.FindObjectsOfTypeAll(buildProfileType)) + { + // BuildTarget.PS5 = 44. + var buildTarget = new SerializedObject(profile).FindProperty("m_BuildTarget")?.intValue ?? -1; + if (buildTarget != 44) + continue; + + var platformSettings = buildProfileType + .GetProperty("platformBuildProfile", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(profile); + + GetFieldInHierarchy(platformSettings?.GetType(), "m_Development")?.SetValue(platformSettings, false); + GetFieldInHierarchy(platformSettings?.GetType(), "m_BuildSubtarget")?.SetValue(platformSettings, 1); // 1 = Package + + EditorUtility.SetDirty(profile); + Debug.Log("Builder: PS5 Build Profile set to Package"); + } + + AssetDatabase.SaveAssets(); + } + + private static void SetSwitchCreateNspRomFile() + { + var buildProfileType = Type.GetType("UnityEditor.Build.Profile.BuildProfile, UnityEditor.CoreModule"); + if (buildProfileType == null) + { + return; + } + + foreach (var profile in Resources.FindObjectsOfTypeAll(buildProfileType)) + { + // BuildTarget.Switch = 38. + var buildTarget = new SerializedObject(profile).FindProperty("m_BuildTarget")?.intValue ?? -1; + if (buildTarget != 38) + continue; + + var platformSettings = buildProfileType + .GetProperty("platformBuildProfile", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(profile); + + GetFieldInHierarchy(platformSettings?.GetType(), "m_Development")?.SetValue(platformSettings, false); + GetFieldInHierarchy(platformSettings?.GetType(), "m_SwitchCreateRomFile")?.SetValue(platformSettings, 1); // 1 = enabled + + EditorUtility.SetDirty(profile); + Debug.Log("Builder: Switch Build Profile set to Create NSP ROM File"); + } + + AssetDatabase.SaveAssets(); + } + + private static FieldInfo GetFieldInHierarchy(Type type, string fieldName) + { + while (type != null) + { + var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (field != null) + return field; + type = type.BaseType; + } + return null; + } + private static void ValidateArguments(Dictionary args) { Debug.Log("Builder: Validating command line arguments"); if (!args.ContainsKey("buildPath") || string.IsNullOrWhiteSpace(args["buildPath"])) { - throw new Exception("No valid '-buildPath' has been provided."); + args["buildPath"] = "./Builds/"; + Debug.Log("Builder: No '-buildPath' provided, defaulting to './Builds/'"); } } diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 7f8352899..4e3dd43d5 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; +using System.IO; using Sentry; +using Sentry.Extensibility; using Sentry.Unity; using UnityEngine; @@ -11,6 +14,7 @@ public override void Configure(SentryUnityOptions options) // 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. + // At test-run time, Integration.Tests.ps1 also reads SENTRY_DSN to verify events. options.Environment = "integration-test"; options.Release = "sentry-unity-test@1.0.0"; @@ -19,6 +23,7 @@ public override void Configure(SentryUnityOptions options) options.AttachScreenshot = true; options.Debug = true; options.DiagnosticLevel = SentryLevel.Debug; + options.DiagnosticLogger = Logger.Instance; options.TracesSampleRate = 1.0d; // No custom HTTP handler -- events go to real sentry.io diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 04466da32..95180a796 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Sentry; @@ -16,17 +17,17 @@ public class IntegrationTester : MonoBehaviour { private void Awake() { - Debug.Log("IntegrationTester, awake!"); + Logger.Log("IntegrationTester, awake!"); Application.quitting += () => { - Debug.Log("IntegrationTester is quitting."); + Logger.Log("IntegrationTester is quitting."); }; } public void Start() { var arg = GetTestArg(); - Debug.Log($"IntegrationTester arg: '{arg}'"); + Logger.Log($"IntegrationTester arg: '{arg}'"); switch (arg) { @@ -43,7 +44,7 @@ public void Start() CrashSend(); break; default: - Debug.LogError($"IntegrationTester: Unknown command: {arg}"); + Logger.LogError($"IntegrationTester: Unknown command: {arg}"); #if !UNITY_WEBGL Application.Quit(1); #endif @@ -105,7 +106,7 @@ private IEnumerator MessageCapture() AddIntegrationTestContext("message-capture"); var eventId = SentrySdk.CaptureMessage("Integration test message"); - Debug.Log($"EVENT_CAPTURED: {eventId}"); + Logger.Log($"EVENT_CAPTURED: {eventId}"); yield return CompleteAndQuit(); } @@ -121,7 +122,7 @@ private IEnumerator ExceptionCapture() catch (Exception ex) { var eventId = SentrySdk.CaptureException(ex); - Debug.Log($"EVENT_CAPTURED: {eventId}"); + Logger.Log($"EVENT_CAPTURED: {eventId}"); } yield return CompleteAndQuit(); @@ -134,9 +135,9 @@ private IEnumerator CompleteAndQuit() // complete. Wait to avoid a race where the test harness shuts down the browser // before the send finishes. yield return new WaitForSeconds(3); - Debug.Log("INTEGRATION_TEST_COMPLETE"); + Logger.Log("INTEGRATION_TEST_COMPLETE"); #else - Debug.Log("INTEGRATION_TEST_COMPLETE"); + Logger.Log("INTEGRATION_TEST_COMPLETE"); Application.Quit(0); yield break; #endif @@ -174,22 +175,22 @@ private IEnumerator CrashCapture() // Wait for the scope sync to complete on platforms that use a background thread (e.g. Android JNI) yield return new WaitForSeconds(0.5f); - Debug.Log($"EVENT_CAPTURED: {crashId}"); - Debug.Log("CRASH TEST: Issuing a native crash (AccessViolation)"); + Logger.Log($"EVENT_CAPTURED: {crashId}"); + Logger.Log("CRASH TEST: Issuing a native crash (AccessViolation)"); Utils.ForceCrash(ForcedCrashCategory.AccessViolation); // Should not reach here - Debug.LogError("CRASH TEST: FAIL - unexpected code executed after crash"); + Logger.Log("ERROR: CRASH TEST: FAIL - unexpected code executed after crash"); Application.Quit(1); } private void CrashSend() { - Debug.Log("CrashSend: Initializing Sentry to flush cached crash report..."); + Logger.Log("CrashSend: Initializing Sentry to flush cached crash report..."); var lastRunState = SentrySdk.GetLastRunState(); - Debug.Log($"CrashSend: crashedLastRun={lastRunState}"); + Logger.Log($"CrashSend: crashedLastRun={lastRunState}"); // Sentry is already initialized by IntegrationOptionsConfiguration. // Just wait a bit for the queued crash report to be sent, then quit. @@ -203,7 +204,7 @@ private IEnumerator WaitAndQuit() SentrySdk.FlushAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); - Debug.Log("CrashSend: Flush complete, quitting."); + Logger.Log("CrashSend: Flush complete, quitting."); Application.Quit(0); } } diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs new file mode 100644 index 000000000..4dbe0634f --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -0,0 +1,153 @@ +using System; +using System.IO; +using Sentry; +using Sentry.Extensibility; +using UnityEngine; + +/// +/// Unified logger for integration tests and the Sentry SDK. +/// +/// On Xbox master (non-development) builds, Debug.Log output is suppressed entirely. +/// This class writes directly to a file via StreamWriter, bypassing Unity's logger +/// so that test output (EVENT_CAPTURED lines, status messages) and Sentry SDK +/// diagnostic messages all end up in a retrievable file. +/// +/// On other platforms, messages go through Debug.Log as usual. +/// +/// Implements so it can be assigned to +/// options.DiagnosticLogger, routing SDK diagnostic output through the same +/// log file without needing a separate logger. +/// +public class Logger : IDiagnosticLogger +{ + private static StreamWriter s_writer; + private static readonly object s_lock = new(); + private static string s_logFilePath; + private SentryLevel _minLevel = SentryLevel.Debug; + + // Instance must be declared after s_lock — static fields initialize in textual order. + public static readonly Logger Instance = CreateInstance(); + + private static Logger CreateInstance() + { +#if UNITY_GAMECORE && !UNITY_EDITOR + Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); +#endif + return new Logger(); + } + + /// + /// Opens the log file. Call once during initialization. + /// Throws if the file cannot be created — the caller should let the app crash + /// so the test harness can detect the non-zero exit code. + /// + private static void Open(string logFilePath) + { + lock (s_lock) + { + if (s_writer != null) + { + return; + } + + var directory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + s_writer = new StreamWriter(logFilePath, append: false) { AutoFlush = true }; + s_logFilePath = logFilePath; + } + } + + /// + /// Returns the path that was opened, or null if not opened. + /// + public static string GetLogFilePath() + { + return s_logFilePath; + } + + /// + /// Writes a line to the log file and Debug.Log. + /// Safe to call even if the file was never opened — the message still goes to Debug.Log. + /// + public static void Log(string message) + { + Debug.Log(message); + WriteToFile(message); + } + + /// + /// Writes a warning to the log file and Debug.LogWarning. + /// + public static void LogWarning(string message) + { + Debug.LogWarning(message); + WriteToFile($"[WARNING] {message}"); + } + + /// + /// Writes an error to the log file and Debug.LogError. + /// + public static void LogError(string message) + { + Debug.LogError(message); + WriteToFile($"[ERROR] {message}"); + } + + private static void WriteToFile(string message) + { + lock (s_lock) + { + if (s_writer == null) + { + return; + } + + try + { + s_writer.WriteLine(message); + } + catch + { + // Don't let file writing errors break the app. + } + } + } + + // --- IDiagnosticLogger (explicit implementation to avoid collision with static Log) --- + + bool IDiagnosticLogger.IsEnabled(SentryLevel level) => level >= _minLevel; + + void IDiagnosticLogger.Log(SentryLevel logLevel, string message, Exception exception, params object[] args) + { + if (!((IDiagnosticLogger)this).IsEnabled(logLevel)) + { + return; + } + + var formatted = args?.Length > 0 ? string.Format(message, args) : message; + if (exception != null) + { + formatted = $"{formatted} {exception}"; + } + + var line = $"[Sentry] ({logLevel}) {formatted}"; + + switch (logLevel) + { + case SentryLevel.Error: + case SentryLevel.Fatal: + LogError(line); + break; + case SentryLevel.Warning: + LogWarning(line); + break; + default: + Log(line); + break; + } + } +} diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs.meta b/test/Scripts.Integration.Test/Scripts/Logger.cs.meta new file mode 100644 index 000000000..69999fcd4 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/Logger.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/integration-test.ps1 b/test/Scripts.Integration.Test/integration-test.ps1 index 06a0b2716..cba9e984e 100644 --- a/test/Scripts.Integration.Test/integration-test.ps1 +++ b/test/Scripts.Integration.Test/integration-test.ps1 @@ -108,9 +108,10 @@ Else { "^Switch$" { Write-PhaseSuccess "Switch build completed - no automated test execution available" } - "^(XSX|XB1)$" - { - Write-PhaseSuccess "Xbox build completed - no automated test execution available" + "^(XSX|XB1)$" { + $env:SENTRY_TEST_PLATFORM = "Xbox" + $env:SENTRY_TEST_APP = GetNewProjectBuildPath + Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI } "^PS5$" {