From 6e98be7dbd85ff30334c4a12b27ff8dfd8acc817 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 20 Mar 2026 11:13:24 +0100 Subject: [PATCH 01/33] added xbox --- test/IntegrationTest/Integration.Tests.ps1 | 27 ++++++++++++++++--- .../integration-test.ps1 | 7 ++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index e547dc59a..ad080fb80 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_PLATFORM: target platform (Android, Desktop, iOS, WebGL, Xbox) # SENTRY_TEST_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 package directory containing .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,6 +32,7 @@ BeforeAll { "Android" { return @("-e", "test", $Action) } "Desktop" { return @("--test", $Action, "-logFile", "-") } "iOS" { return @("--test", $Action) } + "Xbox" { return @("--test", $Action) } } } @@ -142,7 +145,7 @@ 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 @@ -195,10 +198,26 @@ 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 + + # Xbox uses packaged .xvc flow — SENTRY_TEST_APP points to the package directory + $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" | Select-Object -First 1 + if (-not $xvcFile) { + throw "No .xvc package found in: $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" } } 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$" { From 5f8bd87f70d8390a4670dafee314ac2372bfdbfa Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 23 Mar 2026 11:14:51 +0100 Subject: [PATCH 02/33] development build for xbox --- test/Scripts.Integration.Test/Editor/Builder.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 1e776efab..d167c72ed 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -182,14 +182,20 @@ public static void BuildSwitchIL2CPPPlayer() public static void BuildXSXIL2CPPPlayer() { - Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player"); - BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode); + Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player (Development)"); + // Development build required on Xbox: console security disables Debug.Log output + // in non-development builds, which prevents the test harness from capturing + // EVENT_CAPTURED markers and other diagnostic output. + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode | BuildOptions.Development); } public static void BuildXB1IL2CPPPlayer() { - Debug.Log("Builder: Building Xbox One IL2CPP Player"); - BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode); + Debug.Log("Builder: Building Xbox One IL2CPP Player (Development)"); + // Development build required on Xbox: console security disables Debug.Log output + // in non-development builds, which prevents the test harness from capturing + // EVENT_CAPTURED markers and other diagnostic output. + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode | BuildOptions.Development); } public static void BuildPS5IL2CPPPlayer() From 2fa8fc1d98dfd85b58b3d4179159796c2296bee6 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 23 Mar 2026 12:36:17 +0100 Subject: [PATCH 03/33] support loose builds --- test/IntegrationTest/Integration.Tests.ps1 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index ad080fb80..f441b0b29 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -205,14 +205,18 @@ BeforeAll { Connect-Device -Platform "Xbox" -Target $env:XBCONNECT_TARGET - # Xbox uses packaged .xvc flow — SENTRY_TEST_APP points to the package directory - $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" | Select-Object -First 1 - if (-not $xvcFile) { - throw "No .xvc package found in: $env:SENTRY_TEST_APP" + $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($xvcFile) { + # Packaged .xvc flow — install and launch via AUMID + Install-DeviceApp -Path $xvcFile.FullName + $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP + Write-Host "Using AUMID: $($script:ExecutablePath)" + } else { + # Loose file deployment (e.g. development builds that can't be packaged) — + # pass the directory to Invoke-DeviceApp which mirrors it to the Xbox via xbrun. + $script:ExecutablePath = $env:SENTRY_TEST_APP + Write-Host "Using loose deployment: $($script:ExecutablePath)" } - Install-DeviceApp -Path $xvcFile.FullName - $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP - Write-Host "Using AUMID: $($script:ExecutablePath)" } "WebGL" { } From 1b923abf75975c27e8bd0c0fcffde8958116143a Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 23 Mar 2026 15:09:58 +0100 Subject: [PATCH 04/33] fileloghandler --- test/IntegrationTest/Integration.Tests.ps1 | 55 +++++++--- .../Editor/Builder.cs | 14 +-- .../Scripts/FileLogHandler.cs | 100 ++++++++++++++++++ 3 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 test/Scripts.Integration.Test/Scripts/FileLogHandler.cs diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index f441b0b29..5822027ed 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -32,7 +32,7 @@ BeforeAll { "Android" { return @("-e", "test", $Action) } "Desktop" { return @("--test", $Action, "-logFile", "-") } "iOS" { return @("--test", $Action) } - "Xbox" { return @("--test", $Action) } + "Xbox" { return @("--test", $Action, "-logFile", "D:\Logs\UnityIntegrationTest.log") } } } @@ -113,6 +113,25 @@ 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. + # Instead, Unity writes to a log file on the device which we retrieve and use as output. + if ($script:Platform -eq "Xbox") { + $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" + New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null + try { + Copy-DeviceItem -DevicePath "D:\Logs\UnityIntegrationTest.log" -Destination $logLocalDir + $logContent = Get-Content "$logLocalDir/UnityIntegrationTest.log" -ErrorAction SilentlyContinue + if ($logContent -and $logContent.Count -gt 0) { + Write-Host "Retrieved Unity log file from Xbox ($($logContent.Count) lines)" + $runResult.Output = $logContent + } else { + Write-Warning "Unity log file was empty or not found on Xbox." + } + } catch { + Write-Warning "Failed to retrieve Unity log file from Xbox: $_" + } + } + # Save result to JSON file $runResult | ConvertTo-Json -Depth 5 | Out-File -FilePath (Get-OutputFilePath "${Action}-result.json") @@ -123,6 +142,22 @@ BeforeAll { $sendArgs = Get-AppArguments -Action "crash-send" $sendResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $sendArgs + # On Xbox, retrieve the log file for crash-send output too + if ($script:Platform -eq "Xbox") { + $sendLogDir = "$PSScriptRoot/results/xbox-logs/crash-send" + New-Item -ItemType Directory -Path $sendLogDir -Force | Out-Null + try { + Copy-DeviceItem -DevicePath "D:\Logs\UnityIntegrationTest.log" -Destination $sendLogDir + $sendLogContent = Get-Content "$sendLogDir/UnityIntegrationTest.log" -ErrorAction SilentlyContinue + if ($sendLogContent -and $sendLogContent.Count -gt 0) { + Write-Host "Retrieved Unity crash-send log file from Xbox ($($sendLogContent.Count) lines)" + $sendResult.Output = $sendLogContent + } + } catch { + Write-Warning "Failed to retrieve Unity crash-send log file from Xbox: $_" + } + } + # Save crash-send result to JSON for debugging $sendResult | ConvertTo-Json -Depth 5 | Out-File -FilePath (Get-OutputFilePath "crash-send-result.json") @@ -205,18 +240,14 @@ BeforeAll { Connect-Device -Platform "Xbox" -Target $env:XBCONNECT_TARGET - $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($xvcFile) { - # Packaged .xvc flow — install and launch via AUMID - Install-DeviceApp -Path $xvcFile.FullName - $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP - Write-Host "Using AUMID: $($script:ExecutablePath)" - } else { - # Loose file deployment (e.g. development builds that can't be packaged) — - # pass the directory to Invoke-DeviceApp which mirrors it to the Xbox via xbrun. - $script:ExecutablePath = $env:SENTRY_TEST_APP - Write-Host "Using loose deployment: $($script:ExecutablePath)" + # Xbox uses packaged .xvc flow — SENTRY_TEST_APP points to the package directory + $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" | Select-Object -First 1 + if (-not $xvcFile) { + throw "No .xvc package found in: $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" { } diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index d167c72ed..1e776efab 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -182,20 +182,14 @@ public static void BuildSwitchIL2CPPPlayer() public static void BuildXSXIL2CPPPlayer() { - Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player (Development)"); - // Development build required on Xbox: console security disables Debug.Log output - // in non-development builds, which prevents the test harness from capturing - // EVENT_CAPTURED markers and other diagnostic output. - BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode | BuildOptions.Development); + Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player"); + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode); } public static void BuildXB1IL2CPPPlayer() { - Debug.Log("Builder: Building Xbox One IL2CPP Player (Development)"); - // Development build required on Xbox: console security disables Debug.Log output - // in non-development builds, which prevents the test harness from capturing - // EVENT_CAPTURED markers and other diagnostic output. - BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode | BuildOptions.Development); + Debug.Log("Builder: Building Xbox One IL2CPP Player"); + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode); } public static void BuildPS5IL2CPPPlayer() diff --git a/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs b/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs new file mode 100644 index 000000000..b821abad2 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using UnityEngine; + +/// +/// Replaces Unity's default log handler with one that writes to a file on disk. +/// On platforms like Xbox, Debug.Log output is suppressed in non-development builds. +/// This handler ensures all log output is captured to a known file path so the test +/// harness can retrieve and inspect it. +/// +/// Activated by passing `-logFile ` on the command line. +/// +public static class FileLogHandler +{ + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Install() + { + var logFilePath = GetLogFileArg(); + if (string.IsNullOrEmpty(logFilePath)) + { + return; + } + + try + { + var directory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var writer = new StreamWriter(logFilePath, append: false) { AutoFlush = true }; + var originalHandler = Debug.unityLogger.logHandler; + Debug.unityLogger.logHandler = new Handler(writer, originalHandler); + + // Can't use Debug.Log here - it would recurse through the handler before it's + // fully set up in all cases, but actually it's fine since we already assigned it. + Debug.Log($"FileLogHandler: Writing logs to {logFilePath}"); + } + catch (Exception ex) + { + // If we can't write to the file, don't break the app — just continue without file logging. + Debug.LogWarning($"FileLogHandler: Failed to initialize log file at '{logFilePath}': {ex.Message}"); + } + } + + private static string GetLogFileArg() + { + var args = Environment.GetCommandLineArgs(); + for (var i = 0; i < args.Length - 1; i++) + { + if (args[i] == "-logFile") + { + return args[i + 1]; + } + } + return null; + } + + private class Handler : ILogHandler + { + private readonly StreamWriter _writer; + private readonly ILogHandler _originalHandler; + + public Handler(StreamWriter writer, ILogHandler originalHandler) + { + _writer = writer; + _originalHandler = originalHandler; + } + + public void LogFormat(LogType logType, UnityEngine.Object context, string format, params object[] args) + { + try + { + var message = args.Length > 0 ? string.Format(format, args) : format; + _writer.WriteLine(message); + } + catch + { + // Don't let file writing errors break the app. + } + + _originalHandler.LogFormat(logType, context, format, args); + } + + public void LogException(Exception exception, UnityEngine.Object context) + { + try + { + _writer.WriteLine(exception.ToString()); + } + catch + { + // Don't let file writing errors break the app. + } + + _originalHandler.LogException(exception, context); + } + } +} From 119d980df2adc1aa50ad5fd9e7e93d05e69dd867 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 23 Mar 2026 16:13:17 +0100 Subject: [PATCH 05/33] no development builds --- test/Scripts.Integration.Test/Editor/Builder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 1e776efab..b5244b411 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. From c67cfc23968fce5aaba4eb9c79f1246a75148f8a Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Thu, 26 Mar 2026 17:01:26 +0100 Subject: [PATCH 06/33] file logging and master mode --- test/IntegrationTest/Integration.Tests.ps1 | 20 +++++++ .../Editor/Builder.cs | 46 +++++++++++++++ .../IntegrationOptionsConfiguration.cs | 58 +++++++++++++++++++ .../create-project.ps1 | 21 +++++++ 4 files changed, 145 insertions(+) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 5822027ed..2ed17b61d 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -130,6 +130,16 @@ BeforeAll { } catch { Write-Warning "Failed to retrieve Unity log file from Xbox: $_" } + try { + Copy-DeviceItem -DevicePath "D:\Logs\sentry-sdk.log" -Destination $logLocalDir + $sdkLogContent = Get-Content "$logLocalDir/sentry-sdk.log" -ErrorAction SilentlyContinue + if ($sdkLogContent -and $sdkLogContent.Count -gt 0) { + Write-Host "Retrieved SDK log file from Xbox ($($sdkLogContent.Count) lines)" + $runResult.Output = @($runResult.Output) + @($sdkLogContent) + } + } catch { + Write-Warning "Failed to retrieve SDK log file from Xbox: $_" + } } # Save result to JSON file @@ -156,6 +166,16 @@ BeforeAll { } catch { Write-Warning "Failed to retrieve Unity crash-send log file from Xbox: $_" } + try { + Copy-DeviceItem -DevicePath "D:\Logs\sentry-sdk.log" -Destination $sendLogDir + $sdkLogContent = Get-Content "$sendLogDir/sentry-sdk.log" -ErrorAction SilentlyContinue + if ($sdkLogContent -and $sdkLogContent.Count -gt 0) { + Write-Host "Retrieved SDK log file from Xbox for crash-send ($($sdkLogContent.Count) lines)" + $sendResult.Output = @($sendResult.Output) + @($sdkLogContent) + } + } catch { + Write-Warning "Failed to retrieve SDK log file from Xbox for crash-send: $_" + } } # Save crash-send result to JSON for debugging diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index b5244b411..0f83a829f 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -184,12 +184,14 @@ public static void BuildSwitchIL2CPPPlayer() public static void BuildXSXIL2CPPPlayer() { Debug.Log("Builder: Building Xbox Series X|S IL2CPP Player"); + SetXboxSubtargetToMaster(); BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode); } public static void BuildXB1IL2CPPPlayer() { Debug.Log("Builder: Building Xbox One IL2CPP Player"); + SetXboxSubtargetToMaster(); BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode); } @@ -199,6 +201,50 @@ public static void BuildPS5IL2CPPPlayer() BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode); } + // We'll likely extend this to also work with PS and Switch + private static void SetXboxSubtargetToMaster() + { + // The actual editor API to set this has bee 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); + EditorUtility.SetDirty(profile); + Debug.Log($"Builder: Xbox Build Profile (BuildTarget {buildTarget}) set to Master"); + } + 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"); diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 7f8352899..83be6b39e 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; @@ -21,6 +24,14 @@ public override void Configure(SentryUnityOptions options) options.DiagnosticLevel = SentryLevel.Debug; options.TracesSampleRate = 1.0d; +#if UNITY_GAMECORE + // On Xbox, Debug.Log output is suppressed in non-development builds, so SDK diagnostic + // logs must be written to a known file path for the test harness to retrieve them. + options.DiagnosticLogger = new SdkFileLogger( + "D:\\Logs\\sentry-sdk.log", + options.DiagnosticLevel); +#endif + // No custom HTTP handler -- events go to real sentry.io // Filtering test output from breadcrumbs @@ -42,4 +53,51 @@ public override void Configure(SentryUnityOptions options) Debug.Log("Sentry: IntegrationOptionsConfig::Configure() finished"); } + + private class SdkFileLogger : IDiagnosticLogger + { + private readonly StreamWriter _writer; + private readonly SentryLevel _minLevel; + + public SdkFileLogger(string logFilePath, SentryLevel minLevel) + { + _minLevel = minLevel; + try + { + var directory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + _writer = new StreamWriter(logFilePath, append: true) { AutoFlush = true }; + } + catch (Exception ex) + { + Debug.LogWarning($"SdkFileLogger: Failed to open log file '{logFilePath}': {ex.Message}"); + } + } + + public bool IsEnabled(SentryLevel level) => level >= _minLevel; + + public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args) + { + if (!IsEnabled(logLevel) || _writer == null) + { + return; + } + + try + { + var text = args.Length == 0 ? message : string.Format(message, args); + var line = exception == null + ? $"[Sentry] {logLevel}: {text}" + : $"[Sentry] {logLevel}: {text}{Environment.NewLine}{exception}"; + _writer.WriteLine(line); + } + catch + { + // Don't let file writing errors break the app. + } + } + } } diff --git a/test/Scripts.Integration.Test/create-project.ps1 b/test/Scripts.Integration.Test/create-project.ps1 index 6bafe4f60..a748b9aab 100644 --- a/test/Scripts.Integration.Test/create-project.ps1 +++ b/test/Scripts.Integration.Test/create-project.ps1 @@ -45,6 +45,27 @@ $projectSettings = $projectSettings -replace "AndroidTargetArchitectures: ?[0-9] $projectSettings = $projectSettings -replace "iPhoneSdkVersion: ?[0-9]+", "iPhoneSdkVersion: 989" $projectSettings | Out-File $projectSettingsPath +# Set Xbox GameCore builds to Master (non-development) when the GameCore module is present. +# The build type is stored in Library/BuildProfiles and is only written to disk when the editor closes, +# so we patch the files directly after project creation while Unity is not running. +$buildProfilesPath = "$(GetNewProjectPath)/Library/BuildProfiles" +If (Test-Path -Path $buildProfilesPath) +{ + foreach ($profileFile in Get-ChildItem "$buildProfilesPath/PlatformProfile.*.asset") + { + $content = Get-Content $profileFile.FullName -Raw + If ($content -match "GameCoreXboxOne|GameCoreScarlett") + { + $updated = $content -replace "m_Development: 1", "m_Development: 0" + If ($updated -ne $content) + { + $updated | Out-File $profileFile.FullName -Encoding utf8 -NoNewline + Write-Detail "Set Master build profile: $($profileFile.Name)" + } + } + } +} + # Add Unity UI package to manifest.json if not already present # Creating a new project via command line doesn't include the Unity UI package by default while creating it via the Hub does. Write-Log "Checking Unity UI package in manifest.json..." From 0206268b20737a5bf50b0d68eb272e7295599e37 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Thu, 26 Mar 2026 17:54:28 +0100 Subject: [PATCH 07/33] logfile path --- .../Scripts/FileLogHandler.cs | 100 ------------------ .../IntegrationOptionsConfiguration.cs | 4 +- 2 files changed, 2 insertions(+), 102 deletions(-) delete mode 100644 test/Scripts.Integration.Test/Scripts/FileLogHandler.cs diff --git a/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs b/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs deleted file mode 100644 index b821abad2..000000000 --- a/test/Scripts.Integration.Test/Scripts/FileLogHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.IO; -using UnityEngine; - -/// -/// Replaces Unity's default log handler with one that writes to a file on disk. -/// On platforms like Xbox, Debug.Log output is suppressed in non-development builds. -/// This handler ensures all log output is captured to a known file path so the test -/// harness can retrieve and inspect it. -/// -/// Activated by passing `-logFile ` on the command line. -/// -public static class FileLogHandler -{ - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Install() - { - var logFilePath = GetLogFileArg(); - if (string.IsNullOrEmpty(logFilePath)) - { - return; - } - - try - { - var directory = Path.GetDirectoryName(logFilePath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - var writer = new StreamWriter(logFilePath, append: false) { AutoFlush = true }; - var originalHandler = Debug.unityLogger.logHandler; - Debug.unityLogger.logHandler = new Handler(writer, originalHandler); - - // Can't use Debug.Log here - it would recurse through the handler before it's - // fully set up in all cases, but actually it's fine since we already assigned it. - Debug.Log($"FileLogHandler: Writing logs to {logFilePath}"); - } - catch (Exception ex) - { - // If we can't write to the file, don't break the app — just continue without file logging. - Debug.LogWarning($"FileLogHandler: Failed to initialize log file at '{logFilePath}': {ex.Message}"); - } - } - - private static string GetLogFileArg() - { - var args = Environment.GetCommandLineArgs(); - for (var i = 0; i < args.Length - 1; i++) - { - if (args[i] == "-logFile") - { - return args[i + 1]; - } - } - return null; - } - - private class Handler : ILogHandler - { - private readonly StreamWriter _writer; - private readonly ILogHandler _originalHandler; - - public Handler(StreamWriter writer, ILogHandler originalHandler) - { - _writer = writer; - _originalHandler = originalHandler; - } - - public void LogFormat(LogType logType, UnityEngine.Object context, string format, params object[] args) - { - try - { - var message = args.Length > 0 ? string.Format(format, args) : format; - _writer.WriteLine(message); - } - catch - { - // Don't let file writing errors break the app. - } - - _originalHandler.LogFormat(logType, context, format, args); - } - - public void LogException(Exception exception, UnityEngine.Object context) - { - try - { - _writer.WriteLine(exception.ToString()); - } - catch - { - // Don't let file writing errors break the app. - } - - _originalHandler.LogException(exception, context); - } - } -} diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 83be6b39e..60fe52fe8 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -26,9 +26,9 @@ public override void Configure(SentryUnityOptions options) #if UNITY_GAMECORE // On Xbox, Debug.Log output is suppressed in non-development builds, so SDK diagnostic - // logs must be written to a known file path for the test harness to retrieve them. + // logs are written to the app's persistent data path for the test harness to retrieve. options.DiagnosticLogger = new SdkFileLogger( - "D:\\Logs\\sentry-sdk.log", + @"D:\SystemScratch\Logs\sentry-sdk.log", options.DiagnosticLevel); #endif From 3ae6fffab386574b2007bffa9c78f4506eedfdd3 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 11:09:23 +0100 Subject: [PATCH 08/33] logs have a new home --- test/IntegrationTest/Integration.Tests.ps1 | 38 +++++-------------- .../IntegrationOptionsConfiguration.cs | 2 +- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 2ed17b61d..40515bed0 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -32,7 +32,7 @@ BeforeAll { "Android" { return @("-e", "test", $Action) } "Desktop" { return @("--test", $Action, "-logFile", "-") } "iOS" { return @("--test", $Action) } - "Xbox" { return @("--test", $Action, "-logFile", "D:\Logs\UnityIntegrationTest.log") } + "Xbox" { return @("--test", $Action) } } } @@ -119,23 +119,13 @@ BeforeAll { $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null try { - Copy-DeviceItem -DevicePath "D:\Logs\UnityIntegrationTest.log" -Destination $logLocalDir - $logContent = Get-Content "$logLocalDir/UnityIntegrationTest.log" -ErrorAction SilentlyContinue + Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test.log" -Destination $logLocalDir + $logContent = Get-Content "$logLocalDir/unity-integration-test.log" -ErrorAction SilentlyContinue if ($logContent -and $logContent.Count -gt 0) { - Write-Host "Retrieved Unity log file from Xbox ($($logContent.Count) lines)" + Write-Host "Retrieved SDK log file from Xbox ($($logContent.Count) lines)" $runResult.Output = $logContent } else { - Write-Warning "Unity log file was empty or not found on Xbox." - } - } catch { - Write-Warning "Failed to retrieve Unity log file from Xbox: $_" - } - try { - Copy-DeviceItem -DevicePath "D:\Logs\sentry-sdk.log" -Destination $logLocalDir - $sdkLogContent = Get-Content "$logLocalDir/sentry-sdk.log" -ErrorAction SilentlyContinue - if ($sdkLogContent -and $sdkLogContent.Count -gt 0) { - Write-Host "Retrieved SDK log file from Xbox ($($sdkLogContent.Count) lines)" - $runResult.Output = @($runResult.Output) + @($sdkLogContent) + Write-Warning "SDK log file was empty or not found on Xbox." } } catch { Write-Warning "Failed to retrieve SDK log file from Xbox: $_" @@ -157,24 +147,14 @@ BeforeAll { $sendLogDir = "$PSScriptRoot/results/xbox-logs/crash-send" New-Item -ItemType Directory -Path $sendLogDir -Force | Out-Null try { - Copy-DeviceItem -DevicePath "D:\Logs\UnityIntegrationTest.log" -Destination $sendLogDir - $sendLogContent = Get-Content "$sendLogDir/UnityIntegrationTest.log" -ErrorAction SilentlyContinue + Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test.log" -Destination $sendLogDir + $sendLogContent = Get-Content "$sendLogDir/unity-integration-test.log" -ErrorAction SilentlyContinue if ($sendLogContent -and $sendLogContent.Count -gt 0) { - Write-Host "Retrieved Unity crash-send log file from Xbox ($($sendLogContent.Count) lines)" + Write-Host "Retrieved SDK crash-send log file from Xbox ($($sendLogContent.Count) lines)" $sendResult.Output = $sendLogContent } } catch { - Write-Warning "Failed to retrieve Unity crash-send log file from Xbox: $_" - } - try { - Copy-DeviceItem -DevicePath "D:\Logs\sentry-sdk.log" -Destination $sendLogDir - $sdkLogContent = Get-Content "$sendLogDir/sentry-sdk.log" -ErrorAction SilentlyContinue - if ($sdkLogContent -and $sdkLogContent.Count -gt 0) { - Write-Host "Retrieved SDK log file from Xbox for crash-send ($($sdkLogContent.Count) lines)" - $sendResult.Output = @($sendResult.Output) + @($sdkLogContent) - } - } catch { - Write-Warning "Failed to retrieve SDK log file from Xbox for crash-send: $_" + Write-Warning "Failed to retrieve SDK crash-send log file from Xbox: $_" } } diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 60fe52fe8..a0bbc859c 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -28,7 +28,7 @@ public override void Configure(SentryUnityOptions options) // On Xbox, Debug.Log output is suppressed in non-development builds, so SDK diagnostic // logs are written to the app's persistent data path for the test harness to retrieve. options.DiagnosticLogger = new SdkFileLogger( - @"D:\SystemScratch\Logs\sentry-sdk.log", + @"D:\Logs\unity-integration-test.log", options.DiagnosticLevel); #endif From 7190270bfe1f5d8c2a19f4113e6478cca3bbec97 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 11:21:34 +0100 Subject: [PATCH 09/33] dedicated logger --- .../Scripts/IntegrationTester.cs | 34 ++++--- .../Scripts/Logger.cs | 92 +++++++++++++++++++ .../Scripts/Logger.cs.meta | 11 +++ 3 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 test/Scripts.Integration.Test/Scripts/Logger.cs create mode 100644 test/Scripts.Integration.Test/Scripts/Logger.cs.meta diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 04466da32..964fe3aee 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -16,17 +16,23 @@ public class IntegrationTester : MonoBehaviour { private void Awake() { - Debug.Log("IntegrationTester, awake!"); +#if UNITY_GAMECORE + // On Xbox, Debug.Log output is suppressed in non-development builds. + // Open a log file so test output is written to a retrievable location on disk. + Logger.Open(@"D:\Logs\unity-integration-test.log"); +#endif + + 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 +49,7 @@ public void Start() CrashSend(); break; default: - Debug.LogError($"IntegrationTester: Unknown command: {arg}"); + Logger.Log($"ERROR: IntegrationTester: Unknown command: {arg}"); #if !UNITY_WEBGL Application.Quit(1); #endif @@ -105,7 +111,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 +127,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 +140,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 +180,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 +209,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..f0bb2b734 --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using UnityEngine; + +/// +/// Shared log writer for integration tests. +/// +/// 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) ends up in a +/// retrievable file. +/// +/// On other platforms, messages go through Debug.Log as usual. +/// +public static class Logger +{ + private static StreamWriter s_writer; + private static readonly object s_lock = new(); + + /// + /// Opens the log file. Call once during initialization. + /// Subsequent calls are ignored if a writer is already open. + /// + public static void Open(string logFilePath) + { + lock (s_lock) + { + if (s_writer != null) + { + return; + } + + try + { + var directory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + s_writer = new StreamWriter(logFilePath, append: false) { AutoFlush = true }; + } + catch (Exception ex) + { + // If we can't write to the file, don't break the app. + Debug.LogWarning($"Logger: Failed to open '{logFilePath}': {ex.Message}"); + } + } + } + + /// + /// 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) + { + // Always attempt Debug.Log — on platforms where it works, this gives us console output. + 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}"); + } + + 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. + } + } + } +} 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: From e1c2ff2169a43301090662e0f8daec7b3a289a01 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 11:22:27 +0100 Subject: [PATCH 10/33] no special config for xbox --- .../IntegrationOptionsConfiguration.cs | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index a0bbc859c..00704c2a1 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -24,14 +24,6 @@ public override void Configure(SentryUnityOptions options) options.DiagnosticLevel = SentryLevel.Debug; options.TracesSampleRate = 1.0d; -#if UNITY_GAMECORE - // On Xbox, Debug.Log output is suppressed in non-development builds, so SDK diagnostic - // logs are written to the app's persistent data path for the test harness to retrieve. - options.DiagnosticLogger = new SdkFileLogger( - @"D:\Logs\unity-integration-test.log", - options.DiagnosticLevel); -#endif - // No custom HTTP handler -- events go to real sentry.io // Filtering test output from breadcrumbs @@ -53,51 +45,4 @@ public override void Configure(SentryUnityOptions options) Debug.Log("Sentry: IntegrationOptionsConfig::Configure() finished"); } - - private class SdkFileLogger : IDiagnosticLogger - { - private readonly StreamWriter _writer; - private readonly SentryLevel _minLevel; - - public SdkFileLogger(string logFilePath, SentryLevel minLevel) - { - _minLevel = minLevel; - try - { - var directory = Path.GetDirectoryName(logFilePath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - _writer = new StreamWriter(logFilePath, append: true) { AutoFlush = true }; - } - catch (Exception ex) - { - Debug.LogWarning($"SdkFileLogger: Failed to open log file '{logFilePath}': {ex.Message}"); - } - } - - public bool IsEnabled(SentryLevel level) => level >= _minLevel; - - public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args) - { - if (!IsEnabled(logLevel) || _writer == null) - { - return; - } - - try - { - var text = args.Length == 0 ? message : string.Format(message, args); - var line = exception == null - ? $"[Sentry] {logLevel}: {text}" - : $"[Sentry] {logLevel}: {text}{Environment.NewLine}{exception}"; - _writer.WriteLine(line); - } - catch - { - // Don't let file writing errors break the app. - } - } - } } From 4f0104addc0063ab944ec2b2b3584d437174f40d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 13:14:11 +0100 Subject: [PATCH 11/33] diagnostics --- test/IntegrationTest/Integration.Tests.ps1 | 129 ++++++++++++++---- .../Scripts/IntegrationTester.cs | 12 +- .../Scripts/Logger.cs | 43 +++--- 3 files changed, 137 insertions(+), 47 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 40515bed0..f26a7ef03 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -36,6 +36,107 @@ BeforeAll { } } + # Run xbdir.exe to list a directory on the Xbox devkit. Returns the output lines. + # Errors are caught and logged rather than thrown — this is a diagnostic tool. + function Invoke-XboxDirListing { + param( + [Parameter(Mandatory=$true)] + [string]$DevicePath + ) + + Write-Host " xbdir x$DevicePath" -ForegroundColor Gray + try { + $output = & xbdir.exe "x$DevicePath" 2>&1 + $output | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + return $output + } catch { + Write-Host " (xbdir failed: $_)" -ForegroundColor Yellow + return @() + } + } + + # Retrieve the integration test log file from Xbox and attach it to the run result. + # The Unity app writes to Application.persistentDataPath/unity-integration-test.log. + # We don't know the exact devkit-accessible path yet, so we search several candidates + # and log directory listings for diagnostics. + function Get-XboxLogOutput { + param( + [Parameter(Mandatory=$true)] + $RunResult, + + [Parameter(Mandatory=$true)] + [string]$Action + ) + + $logFileName = "unity-integration-test.log" + $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" + New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null + + # Check if the app exited with a non-zero code (Logger.Open likely failed) + if ($RunResult.ExitCode -and $RunResult.ExitCode -ne 0) { + Write-Warning "Xbox app exited with code $($RunResult.ExitCode) — the log file may not have been created." + } + + # Candidate paths where Application.persistentDataPath might map on the devkit. + # We try each one via xbcopy and stop at the first success. + $packageFamilyName = $script:ExecutablePath -replace '!.*$', '' + + $candidateDirs = @( + "D:\DevelopmentFiles\$packageFamilyName\LocalState" + "D:\DevelopmentFiles\$packageFamilyName\AC\LocalState" + "D:\Logs" + ) + + $logContent = $null + + foreach ($candidateDir in $candidateDirs) { + Write-Host "Trying to retrieve log from: $candidateDir" -ForegroundColor Yellow + try { + $copyDest = "$logLocalDir/$($candidateDir -replace '[:\\]', '_')" + New-Item -ItemType Directory -Path $copyDest -Force | Out-Null + Copy-DeviceItem -DevicePath "$candidateDir\$logFileName" -Destination $copyDest + $localFile = Join-Path $copyDest $logFileName + if (Test-Path $localFile) { + $logContent = Get-Content $localFile -ErrorAction SilentlyContinue + if ($logContent -and $logContent.Count -gt 0) { + Write-Host "Retrieved log file from $candidateDir ($($logContent.Count) lines)" -ForegroundColor Green + break + } + } + } catch { + Write-Host " Not found at $candidateDir ($_)" -ForegroundColor Gray + } + } + + if (-not $logContent -or $logContent.Count -eq 0) { + # Log file not found — dump directory listings to help diagnose the correct path + Write-Warning "Log file not found at any candidate path. Listing directories for diagnostics:" + Write-Host "::group::Xbox directory diagnostics" + + Write-Host "`n--- D:\ root ---" -ForegroundColor Cyan + Invoke-XboxDirListing "D:\" + + Write-Host "`n--- D:\DevelopmentFiles\ ---" -ForegroundColor Cyan + Invoke-XboxDirListing "D:\DevelopmentFiles\" + + Write-Host "`n--- D:\DevelopmentFiles\$packageFamilyName\ ---" -ForegroundColor Cyan + Invoke-XboxDirListing "D:\DevelopmentFiles\$packageFamilyName\" + + Write-Host "`n--- D:\DevelopmentFiles\$packageFamilyName\AC\ ---" -ForegroundColor Cyan + Invoke-XboxDirListing "D:\DevelopmentFiles\$packageFamilyName\AC\" + + Write-Host "`n--- S:\ root ---" -ForegroundColor Cyan + Invoke-XboxDirListing "S:\" + + Write-Host "::endgroup::" + + throw "Failed to retrieve Xbox log file ($logFileName). See directory diagnostics above." + } + + $RunResult.Output = $logContent + return $RunResult + } + # Run a WebGL test action via headless Chrome function Invoke-WebGLTestAction { param ( @@ -116,20 +217,7 @@ BeforeAll { # On Xbox, console output is not available in non-development builds. # Instead, Unity writes to a log file on the device which we retrieve and use as output. if ($script:Platform -eq "Xbox") { - $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" - New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null - try { - Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test.log" -Destination $logLocalDir - $logContent = Get-Content "$logLocalDir/unity-integration-test.log" -ErrorAction SilentlyContinue - if ($logContent -and $logContent.Count -gt 0) { - Write-Host "Retrieved SDK log file from Xbox ($($logContent.Count) lines)" - $runResult.Output = $logContent - } else { - Write-Warning "SDK log file was empty or not found on Xbox." - } - } catch { - Write-Warning "Failed to retrieve SDK log file from Xbox: $_" - } + $runResult = Get-XboxLogOutput -RunResult $runResult -Action $Action } # Save result to JSON file @@ -144,18 +232,7 @@ BeforeAll { # On Xbox, retrieve the log file for crash-send output too if ($script:Platform -eq "Xbox") { - $sendLogDir = "$PSScriptRoot/results/xbox-logs/crash-send" - New-Item -ItemType Directory -Path $sendLogDir -Force | Out-Null - try { - Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test.log" -Destination $sendLogDir - $sendLogContent = Get-Content "$sendLogDir/unity-integration-test.log" -ErrorAction SilentlyContinue - if ($sendLogContent -and $sendLogContent.Count -gt 0) { - Write-Host "Retrieved SDK crash-send log file from Xbox ($($sendLogContent.Count) lines)" - $sendResult.Output = $sendLogContent - } - } catch { - Write-Warning "Failed to retrieve SDK crash-send log file from Xbox: $_" - } + $sendResult = Get-XboxLogOutput -RunResult $sendResult -Action "crash-send" } # Save crash-send result to JSON for debugging diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 964fe3aee..ea3637dc2 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; @@ -17,9 +18,12 @@ public class IntegrationTester : MonoBehaviour private void Awake() { #if UNITY_GAMECORE - // On Xbox, Debug.Log output is suppressed in non-development builds. - // Open a log file so test output is written to a retrievable location on disk. - Logger.Open(@"D:\Logs\unity-integration-test.log"); + // On Xbox, Debug.Log output is suppressed in non-development (master) builds. + // Write to a file so the test harness can retrieve the output. + var logPath = Path.Combine(Application.persistentDataPath, "unity-integration-test.log"); + Logger.Open(logPath); // Throws on failure — let the app crash so the test harness sees a non-zero exit code. + Logger.Log($"persistentDataPath: {Application.persistentDataPath}"); + Logger.Log($"Log file: {logPath}"); #endif Logger.Log("IntegrationTester, awake!"); @@ -49,7 +53,7 @@ public void Start() CrashSend(); break; default: - Logger.Log($"ERROR: IntegrationTester: Unknown command: {arg}"); + Logger.LogError($"IntegrationTester: Unknown command: {arg}"); #if !UNITY_WEBGL Application.Quit(1); #endif diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index f0bb2b734..aace32c3a 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -16,10 +16,12 @@ public static class Logger { private static StreamWriter s_writer; private static readonly object s_lock = new(); + private static string s_logFilePath; /// /// Opens the log file. Call once during initialization. - /// Subsequent calls are ignored if a writer is already open. + /// 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. /// public static void Open(string logFilePath) { @@ -30,33 +32,32 @@ public static void Open(string logFilePath) return; } - try + var directory = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { - var directory = Path.GetDirectoryName(logFilePath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - s_writer = new StreamWriter(logFilePath, append: false) { AutoFlush = true }; - } - catch (Exception ex) - { - // If we can't write to the file, don't break the app. - Debug.LogWarning($"Logger: Failed to open '{logFilePath}': {ex.Message}"); + 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) { - // Always attempt Debug.Log — on platforms where it works, this gives us console output. Debug.Log(message); - WriteToFile(message); } @@ -66,10 +67,18 @@ public static void Log(string message) 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) From 1d73ec0eaba429e332a27881a0fa1f5ab5b290c8 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 14:08:55 +0100 Subject: [PATCH 12/33] more diagnostics --- test/IntegrationTest/Integration.Tests.ps1 | 26 ++++++++++-- .../Scripts/IntegrationTester.cs | 41 +++++++++++++++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index f26a7ef03..4f46e6339 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -36,7 +36,7 @@ BeforeAll { } } - # Run xbdir.exe to list a directory on the Xbox devkit. Returns the output lines. + # List a directory on the Xbox devkit by mirroring it to a local temp folder. # Errors are caught and logged rather than thrown — this is a diagnostic tool. function Invoke-XboxDirListing { param( @@ -44,14 +44,31 @@ BeforeAll { [string]$DevicePath ) - Write-Host " xbdir x$DevicePath" -ForegroundColor Gray + Write-Host " Listing x$DevicePath" -ForegroundColor Gray + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "xbox-diag-$([System.IO.Path]::GetRandomFileName())" try { - $output = & xbdir.exe "x$DevicePath" 2>&1 + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + # xbcopy mirrors the directory — even if it fails, the output shows the error/path info + $output = & xbcopy.exe "x$DevicePath" "$tempDir" /mirror 2>&1 $output | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + + # List whatever got copied locally + if (Test-Path $tempDir) { + $items = Get-ChildItem $tempDir -Recurse -ErrorAction SilentlyContinue + if ($items) { + Write-Host " Contents:" -ForegroundColor Gray + $items | ForEach-Object { + $rel = $_.FullName.Substring($tempDir.Length) + Write-Host " $rel ($($_.Length) bytes)" -ForegroundColor Gray + } + } + } return $output } catch { - Write-Host " (xbdir failed: $_)" -ForegroundColor Yellow + Write-Host " (listing failed: $_)" -ForegroundColor Yellow return @() + } finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } @@ -85,6 +102,7 @@ BeforeAll { "D:\DevelopmentFiles\$packageFamilyName\LocalState" "D:\DevelopmentFiles\$packageFamilyName\AC\LocalState" "D:\Logs" + "T:" ) $logContent = $null diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index ea3637dc2..fbe95d9df 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -20,10 +20,43 @@ private void Awake() #if UNITY_GAMECORE // On Xbox, Debug.Log output is suppressed in non-development (master) builds. // Write to a file so the test harness can retrieve the output. - var logPath = Path.Combine(Application.persistentDataPath, "unity-integration-test.log"); - Logger.Open(logPath); // Throws on failure — let the app crash so the test harness sees a non-zero exit code. - Logger.Log($"persistentDataPath: {Application.persistentDataPath}"); - Logger.Log($"Log file: {logPath}"); + // Try several candidate paths — we don't know which ones are writable in a packaged master build. + var logFileName = "unity-integration-test.log"; + var candidatePaths = new[] + { + Path.Combine(Application.persistentDataPath, logFileName), + Path.Combine(Application.temporaryCachePath, logFileName), + @"D:\Logs\" + logFileName, + @"T:\" + logFileName, + }; + + string openedPath = null; + string allErrors = ""; + foreach (var candidate in candidatePaths) + { + try + { + Logger.Open(candidate); + openedPath = candidate; + break; + } + catch (Exception ex) + { + allErrors += $" {candidate}: {ex.Message}\n"; + } + } + + if (openedPath != null) + { + Logger.Log($"Log file opened at: {openedPath}"); + Logger.Log($"persistentDataPath: {Application.persistentDataPath}"); + Logger.Log($"temporaryCachePath: {Application.temporaryCachePath}"); + } + else + { + // None of the paths worked — crash so the test harness sees a non-zero exit code. + throw new IOException($"Failed to open log file at any candidate path:\n{allErrors}"); + } #endif Logger.Log("IntegrationTester, awake!"); From a75bf9d1a6776c64909630f73aef98f3b62222aa Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 14:34:32 +0100 Subject: [PATCH 13/33] try and see --- test/IntegrationTest/Integration.Tests.ps1 | 58 ++++++++++++++++++- .../Scripts/IntegrationTester.cs | 12 ++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 4f46e6339..bb68b15cf 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -46,10 +46,11 @@ BeforeAll { Write-Host " Listing x$DevicePath" -ForegroundColor Gray $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "xbox-diag-$([System.IO.Path]::GetRandomFileName())" + $xbcopyPath = if ($env:GameDK) { Join-Path $env:GameDK 'bin\xbcopy.exe' } else { 'xbcopy.exe' } try { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # xbcopy mirrors the directory — even if it fails, the output shows the error/path info - $output = & xbcopy.exe "x$DevicePath" "$tempDir" /mirror 2>&1 + $output = & $xbcopyPath "x$DevicePath" "$tempDir" /mirror 2>&1 $output | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } # List whatever got copied locally @@ -94,13 +95,51 @@ BeforeAll { Write-Warning "Xbox app exited with code $($RunResult.ExitCode) — the log file may not have been created." } - # Candidate paths where Application.persistentDataPath might map on the devkit. - # We try each one via xbcopy and stop at the first success. + # First, check if the app wrote a breadcrumb file telling us where the log is. $packageFamilyName = $script:ExecutablePath -replace '!.*$', '' + try { + $breadcrumbDest = "$logLocalDir/breadcrumb" + New-Item -ItemType Directory -Path $breadcrumbDest -Force | Out-Null + Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test-path.txt" -Destination $breadcrumbDest + $breadcrumbFile = Join-Path $breadcrumbDest "unity-integration-test-path.txt" + if (Test-Path $breadcrumbFile) { + $discoveredPath = (Get-Content $breadcrumbFile -Raw).Trim() + if ($discoveredPath) { + Write-Host "Breadcrumb says log file is at: $discoveredPath" -ForegroundColor Cyan + # Extract the directory from the discovered path + $discoveredDir = Split-Path $discoveredPath -Parent + # Try to retrieve using this exact path + try { + $directDest = "$logLocalDir/discovered" + New-Item -ItemType Directory -Path $directDest -Force | Out-Null + Copy-DeviceItem -DevicePath $discoveredPath -Destination $directDest + $localFile = Join-Path $directDest $logFileName + if (Test-Path $localFile) { + $logContent = Get-Content $localFile -ErrorAction SilentlyContinue + if ($logContent -and $logContent.Count -gt 0) { + Write-Host "Retrieved log file via breadcrumb ($($logContent.Count) lines)" -ForegroundColor Green + $RunResult.Output = $logContent + return $RunResult + } + } + } catch { + Write-Host " Could not retrieve from breadcrumb path: $_" -ForegroundColor Yellow + } + } + } + } catch { + Write-Host "No breadcrumb file found at D:\Logs\ ($_)" -ForegroundColor Gray + } + + # Fallback: try candidate paths where Application.persistentDataPath might map on the devkit. + # We try each one via xbcopy and stop at the first success. $candidateDirs = @( "D:\DevelopmentFiles\$packageFamilyName\LocalState" "D:\DevelopmentFiles\$packageFamilyName\AC\LocalState" + "D:\DevelopmentFiles\$packageFamilyName\TempState" + "D:\DevelopmentFiles\$packageFamilyName\AC\TempState" + "D:\DevelopmentFiles\$packageFamilyName\AC\Temp" "D:\Logs" "T:" ) @@ -114,6 +153,13 @@ BeforeAll { New-Item -ItemType Directory -Path $copyDest -Force | Out-Null Copy-DeviceItem -DevicePath "$candidateDir\$logFileName" -Destination $copyDest $localFile = Join-Path $copyDest $logFileName + # Show what we got locally + $localItems = Get-ChildItem $copyDest -ErrorAction SilentlyContinue + if ($localItems) { + Write-Host " Copied files: $($localItems | ForEach-Object { "$($_.Name) ($($_.Length) bytes)" })" -ForegroundColor Gray + } else { + Write-Host " Copy succeeded but directory is empty" -ForegroundColor Gray + } if (Test-Path $localFile) { $logContent = Get-Content $localFile -ErrorAction SilentlyContinue if ($logContent -and $logContent.Count -gt 0) { @@ -143,6 +189,12 @@ BeforeAll { Write-Host "`n--- D:\DevelopmentFiles\$packageFamilyName\AC\ ---" -ForegroundColor Cyan Invoke-XboxDirListing "D:\DevelopmentFiles\$packageFamilyName\AC\" + Write-Host "`n--- D:\Logs\ ---" -ForegroundColor Cyan + Invoke-XboxDirListing "D:\Logs\" + + Write-Host "`n--- T:\ root ---" -ForegroundColor Cyan + Invoke-XboxDirListing "T:\" + Write-Host "`n--- S:\ root ---" -ForegroundColor Cyan Invoke-XboxDirListing "S:\" diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index fbe95d9df..930de5a93 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -51,6 +51,18 @@ private void Awake() Logger.Log($"Log file opened at: {openedPath}"); Logger.Log($"persistentDataPath: {Application.persistentDataPath}"); Logger.Log($"temporaryCachePath: {Application.temporaryCachePath}"); + + // Write a breadcrumb file to D:\Logs so the test harness can discover where the log ended up. + // D:\Logs is known to be accessible via xbcopy even if the app can't write the main log there. + try + { + Directory.CreateDirectory(@"D:\Logs"); + File.WriteAllText(@"D:\Logs\unity-integration-test-path.txt", openedPath); + } + catch + { + // Best-effort — if D:\Logs isn't writable either, we'll rely on candidate search. + } } else { From 244396ea43f358e992d4f533a15ca7cd1f3336d4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 27 Mar 2026 15:26:28 +0100 Subject: [PATCH 14/33] diagnostics --- test/IntegrationTest/Integration.Tests.ps1 | 51 +++++++++++++++---- .../Scripts/IntegrationTester.cs | 51 +++++++++++++------ 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index bb68b15cf..bf3480ddf 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -131,17 +131,19 @@ BeforeAll { Write-Host "No breadcrumb file found at D:\Logs\ ($_)" -ForegroundColor Gray } - # Fallback: try candidate paths where Application.persistentDataPath might map on the devkit. - # We try each one via xbcopy and stop at the first success. + # Fallback: try candidate paths matching the C# candidate order in IntegrationTester.cs. + # D:\Logs is first because it's the primary candidate on the C# side and is known to be + # xbcopy-accessible. Then try paths where persistentDataPath/temporaryCachePath might + # resolve, plus the D:\ root as a last resort. $candidateDirs = @( + "D:\Logs" "D:\DevelopmentFiles\$packageFamilyName\LocalState" "D:\DevelopmentFiles\$packageFamilyName\AC\LocalState" "D:\DevelopmentFiles\$packageFamilyName\TempState" "D:\DevelopmentFiles\$packageFamilyName\AC\TempState" "D:\DevelopmentFiles\$packageFamilyName\AC\Temp" - "D:\Logs" - "T:" + "D:\" ) $logContent = $null @@ -173,9 +175,39 @@ BeforeAll { } if (-not $logContent -or $logContent.Count -eq 0) { - # Log file not found — dump directory listings to help diagnose the correct path - Write-Warning "Log file not found at any candidate path. Listing directories for diagnostics:" - Write-Host "::group::Xbox directory diagnostics" + # Log file not found — try to retrieve the diagnostic file the app writes before crashing + Write-Warning "Log file not found at any candidate path. Checking for diagnostic files..." + Write-Host "::group::Xbox diagnostics" + + # The C# code writes D:\unity-integration-test-diag.txt when all candidates fail + try { + $diagDest = "$logLocalDir/diag" + New-Item -ItemType Directory -Path $diagDest -Force | Out-Null + Copy-DeviceItem -DevicePath "D:\unity-integration-test-diag.txt" -Destination $diagDest + $diagFile = Join-Path $diagDest "unity-integration-test-diag.txt" + if (Test-Path $diagFile) { + Write-Host "`n--- App diagnostic output (D:\unity-integration-test-diag.txt) ---" -ForegroundColor Red + Get-Content $diagFile | ForEach-Object { Write-Host " $_" -ForegroundColor Red } + } + } catch { + Write-Host "No diagnostic file found at D:\unity-integration-test-diag.txt ($_)" -ForegroundColor Gray + } + + # Also retrieve the Xbox crash log file if present + try { + $crashLogDest = "$logLocalDir/crash" + New-Item -ItemType Directory -Path $crashLogDest -Force | Out-Null + Copy-DeviceItem -DevicePath "D:\FullExceptionLogFile.txt" -Destination $crashLogDest + $crashLogFile = Join-Path $crashLogDest "FullExceptionLogFile.txt" + if (Test-Path $crashLogFile) { + Write-Host "`n--- Xbox crash log (D:\FullExceptionLogFile.txt) ---" -ForegroundColor Red + Get-Content $crashLogFile | ForEach-Object { Write-Host " $_" -ForegroundColor Red } + } + } catch { + Write-Host "No crash log at D:\FullExceptionLogFile.txt ($_)" -ForegroundColor Gray + } + + Write-Host "`nDirectory listings for diagnostics:" Write-Host "`n--- D:\ root ---" -ForegroundColor Cyan Invoke-XboxDirListing "D:\" @@ -192,15 +224,12 @@ BeforeAll { Write-Host "`n--- D:\Logs\ ---" -ForegroundColor Cyan Invoke-XboxDirListing "D:\Logs\" - Write-Host "`n--- T:\ root ---" -ForegroundColor Cyan - Invoke-XboxDirListing "T:\" - Write-Host "`n--- S:\ root ---" -ForegroundColor Cyan Invoke-XboxDirListing "S:\" Write-Host "::endgroup::" - throw "Failed to retrieve Xbox log file ($logFileName). See directory diagnostics above." + throw "Failed to retrieve Xbox log file ($logFileName). See diagnostics above." } $RunResult.Output = $logContent diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 930de5a93..9327a6d69 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -19,16 +19,26 @@ private void Awake() { #if UNITY_GAMECORE // On Xbox, Debug.Log output is suppressed in non-development (master) builds. - // Write to a file so the test harness can retrieve the output. - // Try several candidate paths — we don't know which ones are writable in a packaged master build. + // Write to a file so the test harness can retrieve the output via xbcopy. + // + // Candidate paths are ordered by likelihood of working on a retail devkit: + // 1. D:\Logs\ — known to be xbcopy-accessible, other apps (SentryPlayground) write here + // 2. persistentDataPath — may resolve to a sandbox path that doesn't exist in master builds + // 3. temporaryCachePath — same concern as persistentDataPath + // 4. D:\ root — crash dumps land here, so it's writable var logFileName = "unity-integration-test.log"; - var candidatePaths = new[] - { - Path.Combine(Application.persistentDataPath, logFileName), - Path.Combine(Application.temporaryCachePath, logFileName), - @"D:\Logs\" + logFileName, - @"T:\" + logFileName, - }; + string persistentPath = null; + string tempCachePath = null; + try { persistentPath = Application.persistentDataPath; } catch { /* may throw on some configs */ } + try { tempCachePath = Application.temporaryCachePath; } catch { /* may throw on some configs */ } + + var candidatePaths = new List(); + candidatePaths.Add(@"D:\Logs\" + logFileName); + if (!string.IsNullOrEmpty(persistentPath)) + candidatePaths.Add(Path.Combine(persistentPath, logFileName)); + if (!string.IsNullOrEmpty(tempCachePath)) + candidatePaths.Add(Path.Combine(tempCachePath, logFileName)); + candidatePaths.Add(@"D:\" + logFileName); string openedPath = null; string allErrors = ""; @@ -42,18 +52,17 @@ private void Awake() } catch (Exception ex) { - allErrors += $" {candidate}: {ex.Message}\n"; + allErrors += $" {candidate}: {ex.GetType().Name}: {ex.Message}\n"; } } if (openedPath != null) { Logger.Log($"Log file opened at: {openedPath}"); - Logger.Log($"persistentDataPath: {Application.persistentDataPath}"); - Logger.Log($"temporaryCachePath: {Application.temporaryCachePath}"); + Logger.Log($"persistentDataPath: {persistentPath ?? "(null)"}"); + Logger.Log($"temporaryCachePath: {tempCachePath ?? "(null)"}"); // Write a breadcrumb file to D:\Logs so the test harness can discover where the log ended up. - // D:\Logs is known to be accessible via xbcopy even if the app can't write the main log there. try { Directory.CreateDirectory(@"D:\Logs"); @@ -61,13 +70,23 @@ private void Awake() } catch { - // Best-effort — if D:\Logs isn't writable either, we'll rely on candidate search. + // Best-effort — if D:\Logs isn't writable, the test harness will use candidate search. } } else { - // None of the paths worked — crash so the test harness sees a non-zero exit code. - throw new IOException($"Failed to open log file at any candidate path:\n{allErrors}"); + // None of the paths worked. Write diagnostics to D:\ before crashing so the test + // harness can retrieve the file via xbcopy and see what went wrong. + var diagMessage = $"Failed to open log file at any candidate path:\n{allErrors}"; + try + { + File.WriteAllText(@"D:\unity-integration-test-diag.txt", diagMessage); + } + catch + { + // If even D:\ root isn't writable, the crash dump is our only clue. + } + throw new IOException(diagMessage); } #endif From 5b454ba2287ef9ce57d74ec917caf094ffa851e6 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Mon, 30 Mar 2026 12:55:36 +0200 Subject: [PATCH 15/33] found the path --- test/IntegrationTest/Integration.Tests.ps1 | 187 +----------------- .../Scripts/IntegrationTester.cs | 72 +------ 2 files changed, 10 insertions(+), 249 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index bf3480ddf..f80f0c399 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -36,47 +36,8 @@ BeforeAll { } } - # List a directory on the Xbox devkit by mirroring it to a local temp folder. - # Errors are caught and logged rather than thrown — this is a diagnostic tool. - function Invoke-XboxDirListing { - param( - [Parameter(Mandatory=$true)] - [string]$DevicePath - ) - - Write-Host " Listing x$DevicePath" -ForegroundColor Gray - $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "xbox-diag-$([System.IO.Path]::GetRandomFileName())" - $xbcopyPath = if ($env:GameDK) { Join-Path $env:GameDK 'bin\xbcopy.exe' } else { 'xbcopy.exe' } - try { - New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - # xbcopy mirrors the directory — even if it fails, the output shows the error/path info - $output = & $xbcopyPath "x$DevicePath" "$tempDir" /mirror 2>&1 - $output | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - - # List whatever got copied locally - if (Test-Path $tempDir) { - $items = Get-ChildItem $tempDir -Recurse -ErrorAction SilentlyContinue - if ($items) { - Write-Host " Contents:" -ForegroundColor Gray - $items | ForEach-Object { - $rel = $_.FullName.Substring($tempDir.Length) - Write-Host " $rel ($($_.Length) bytes)" -ForegroundColor Gray - } - } - } - return $output - } catch { - Write-Host " (listing failed: $_)" -ForegroundColor Yellow - return @() - } finally { - Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue - } - } - # Retrieve the integration test log file from Xbox and attach it to the run result. - # The Unity app writes to Application.persistentDataPath/unity-integration-test.log. - # We don't know the exact devkit-accessible path yet, so we search several candidates - # and log directory listings for diagnostics. + # The Unity app writes to D:\Logs\UnityIntegrationTest.log on Xbox (UNITY_GAMECORE). function Get-XboxLogOutput { param( [Parameter(Mandatory=$true)] @@ -86,152 +47,23 @@ BeforeAll { [string]$Action ) - $logFileName = "unity-integration-test.log" + $logFileName = "UnityIntegrationTest.log" $logLocalDir = "$PSScriptRoot/results/xbox-logs/$Action" New-Item -ItemType Directory -Path $logLocalDir -Force | Out-Null - # Check if the app exited with a non-zero code (Logger.Open likely failed) - if ($RunResult.ExitCode -and $RunResult.ExitCode -ne 0) { - Write-Warning "Xbox app exited with code $($RunResult.ExitCode) — the log file may not have been created." - } - - # First, check if the app wrote a breadcrumb file telling us where the log is. - $packageFamilyName = $script:ExecutablePath -replace '!.*$', '' try { - $breadcrumbDest = "$logLocalDir/breadcrumb" - New-Item -ItemType Directory -Path $breadcrumbDest -Force | Out-Null - Copy-DeviceItem -DevicePath "D:\Logs\unity-integration-test-path.txt" -Destination $breadcrumbDest - $breadcrumbFile = Join-Path $breadcrumbDest "unity-integration-test-path.txt" - if (Test-Path $breadcrumbFile) { - $discoveredPath = (Get-Content $breadcrumbFile -Raw).Trim() - if ($discoveredPath) { - Write-Host "Breadcrumb says log file is at: $discoveredPath" -ForegroundColor Cyan - # Extract the directory from the discovered path - $discoveredDir = Split-Path $discoveredPath -Parent - # Try to retrieve using this exact path - try { - $directDest = "$logLocalDir/discovered" - New-Item -ItemType Directory -Path $directDest -Force | Out-Null - Copy-DeviceItem -DevicePath $discoveredPath -Destination $directDest - $localFile = Join-Path $directDest $logFileName - if (Test-Path $localFile) { - $logContent = Get-Content $localFile -ErrorAction SilentlyContinue - if ($logContent -and $logContent.Count -gt 0) { - Write-Host "Retrieved log file via breadcrumb ($($logContent.Count) lines)" -ForegroundColor Green - $RunResult.Output = $logContent - return $RunResult - } - } - } catch { - Write-Host " Could not retrieve from breadcrumb path: $_" -ForegroundColor Yellow - } - } - } + Copy-DeviceItem -DevicePath "D:\Logs\$logFileName" -Destination $logLocalDir } catch { - Write-Host "No breadcrumb file found at D:\Logs\ ($_)" -ForegroundColor Gray - } - - # Fallback: try candidate paths matching the C# candidate order in IntegrationTester.cs. - # D:\Logs is first because it's the primary candidate on the C# side and is known to be - # xbcopy-accessible. Then try paths where persistentDataPath/temporaryCachePath might - # resolve, plus the D:\ root as a last resort. - - $candidateDirs = @( - "D:\Logs" - "D:\DevelopmentFiles\$packageFamilyName\LocalState" - "D:\DevelopmentFiles\$packageFamilyName\AC\LocalState" - "D:\DevelopmentFiles\$packageFamilyName\TempState" - "D:\DevelopmentFiles\$packageFamilyName\AC\TempState" - "D:\DevelopmentFiles\$packageFamilyName\AC\Temp" - "D:\" - ) - - $logContent = $null - - foreach ($candidateDir in $candidateDirs) { - Write-Host "Trying to retrieve log from: $candidateDir" -ForegroundColor Yellow - try { - $copyDest = "$logLocalDir/$($candidateDir -replace '[:\\]', '_')" - New-Item -ItemType Directory -Path $copyDest -Force | Out-Null - Copy-DeviceItem -DevicePath "$candidateDir\$logFileName" -Destination $copyDest - $localFile = Join-Path $copyDest $logFileName - # Show what we got locally - $localItems = Get-ChildItem $copyDest -ErrorAction SilentlyContinue - if ($localItems) { - Write-Host " Copied files: $($localItems | ForEach-Object { "$($_.Name) ($($_.Length) bytes)" })" -ForegroundColor Gray - } else { - Write-Host " Copy succeeded but directory is empty" -ForegroundColor Gray - } - if (Test-Path $localFile) { - $logContent = Get-Content $localFile -ErrorAction SilentlyContinue - if ($logContent -and $logContent.Count -gt 0) { - Write-Host "Retrieved log file from $candidateDir ($($logContent.Count) lines)" -ForegroundColor Green - break - } - } - } catch { - Write-Host " Not found at $candidateDir ($_)" -ForegroundColor Gray - } + 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) { - # Log file not found — try to retrieve the diagnostic file the app writes before crashing - Write-Warning "Log file not found at any candidate path. Checking for diagnostic files..." - Write-Host "::group::Xbox diagnostics" - - # The C# code writes D:\unity-integration-test-diag.txt when all candidates fail - try { - $diagDest = "$logLocalDir/diag" - New-Item -ItemType Directory -Path $diagDest -Force | Out-Null - Copy-DeviceItem -DevicePath "D:\unity-integration-test-diag.txt" -Destination $diagDest - $diagFile = Join-Path $diagDest "unity-integration-test-diag.txt" - if (Test-Path $diagFile) { - Write-Host "`n--- App diagnostic output (D:\unity-integration-test-diag.txt) ---" -ForegroundColor Red - Get-Content $diagFile | ForEach-Object { Write-Host " $_" -ForegroundColor Red } - } - } catch { - Write-Host "No diagnostic file found at D:\unity-integration-test-diag.txt ($_)" -ForegroundColor Gray - } - - # Also retrieve the Xbox crash log file if present - try { - $crashLogDest = "$logLocalDir/crash" - New-Item -ItemType Directory -Path $crashLogDest -Force | Out-Null - Copy-DeviceItem -DevicePath "D:\FullExceptionLogFile.txt" -Destination $crashLogDest - $crashLogFile = Join-Path $crashLogDest "FullExceptionLogFile.txt" - if (Test-Path $crashLogFile) { - Write-Host "`n--- Xbox crash log (D:\FullExceptionLogFile.txt) ---" -ForegroundColor Red - Get-Content $crashLogFile | ForEach-Object { Write-Host " $_" -ForegroundColor Red } - } - } catch { - Write-Host "No crash log at D:\FullExceptionLogFile.txt ($_)" -ForegroundColor Gray - } - - Write-Host "`nDirectory listings for diagnostics:" - - Write-Host "`n--- D:\ root ---" -ForegroundColor Cyan - Invoke-XboxDirListing "D:\" - - Write-Host "`n--- D:\DevelopmentFiles\ ---" -ForegroundColor Cyan - Invoke-XboxDirListing "D:\DevelopmentFiles\" - - Write-Host "`n--- D:\DevelopmentFiles\$packageFamilyName\ ---" -ForegroundColor Cyan - Invoke-XboxDirListing "D:\DevelopmentFiles\$packageFamilyName\" - - Write-Host "`n--- D:\DevelopmentFiles\$packageFamilyName\AC\ ---" -ForegroundColor Cyan - Invoke-XboxDirListing "D:\DevelopmentFiles\$packageFamilyName\AC\" - - Write-Host "`n--- D:\Logs\ ---" -ForegroundColor Cyan - Invoke-XboxDirListing "D:\Logs\" - - Write-Host "`n--- S:\ root ---" -ForegroundColor Cyan - Invoke-XboxDirListing "S:\" - - Write-Host "::endgroup::" - - throw "Failed to retrieve Xbox log file ($logFileName). See diagnostics above." + throw "Xbox log file was empty or missing (D:\Logs\$logFileName)." } + Write-Host "Retrieved log file from Xbox ($($logContent.Count) lines)" -ForegroundColor Green $RunResult.Output = $logContent return $RunResult } @@ -314,7 +146,7 @@ BeforeAll { $runResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $appArgs # On Xbox, console output is not available in non-development builds. - # Instead, Unity writes to a log file on the device which we retrieve and use as output. + # Retrieve the log file the app writes directly to disk. if ($script:Platform -eq "Xbox") { $runResult = Get-XboxLogOutput -RunResult $runResult -Action $Action } @@ -329,7 +161,6 @@ BeforeAll { $sendArgs = Get-AppArguments -Action "crash-send" $sendResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $sendArgs - # On Xbox, retrieve the log file for crash-send output too if ($script:Platform -eq "Xbox") { $sendResult = Get-XboxLogOutput -RunResult $sendResult -Action "crash-send" } diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 9327a6d69..e3015c913 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -18,78 +18,8 @@ public class IntegrationTester : MonoBehaviour private void Awake() { #if UNITY_GAMECORE - // On Xbox, Debug.Log output is suppressed in non-development (master) builds. - // Write to a file so the test harness can retrieve the output via xbcopy. - // - // Candidate paths are ordered by likelihood of working on a retail devkit: - // 1. D:\Logs\ — known to be xbcopy-accessible, other apps (SentryPlayground) write here - // 2. persistentDataPath — may resolve to a sandbox path that doesn't exist in master builds - // 3. temporaryCachePath — same concern as persistentDataPath - // 4. D:\ root — crash dumps land here, so it's writable - var logFileName = "unity-integration-test.log"; - string persistentPath = null; - string tempCachePath = null; - try { persistentPath = Application.persistentDataPath; } catch { /* may throw on some configs */ } - try { tempCachePath = Application.temporaryCachePath; } catch { /* may throw on some configs */ } - - var candidatePaths = new List(); - candidatePaths.Add(@"D:\Logs\" + logFileName); - if (!string.IsNullOrEmpty(persistentPath)) - candidatePaths.Add(Path.Combine(persistentPath, logFileName)); - if (!string.IsNullOrEmpty(tempCachePath)) - candidatePaths.Add(Path.Combine(tempCachePath, logFileName)); - candidatePaths.Add(@"D:\" + logFileName); - - string openedPath = null; - string allErrors = ""; - foreach (var candidate in candidatePaths) - { - try - { - Logger.Open(candidate); - openedPath = candidate; - break; - } - catch (Exception ex) - { - allErrors += $" {candidate}: {ex.GetType().Name}: {ex.Message}\n"; - } - } - - if (openedPath != null) - { - Logger.Log($"Log file opened at: {openedPath}"); - Logger.Log($"persistentDataPath: {persistentPath ?? "(null)"}"); - Logger.Log($"temporaryCachePath: {tempCachePath ?? "(null)"}"); - - // Write a breadcrumb file to D:\Logs so the test harness can discover where the log ended up. - try - { - Directory.CreateDirectory(@"D:\Logs"); - File.WriteAllText(@"D:\Logs\unity-integration-test-path.txt", openedPath); - } - catch - { - // Best-effort — if D:\Logs isn't writable, the test harness will use candidate search. - } - } - else - { - // None of the paths worked. Write diagnostics to D:\ before crashing so the test - // harness can retrieve the file via xbcopy and see what went wrong. - var diagMessage = $"Failed to open log file at any candidate path:\n{allErrors}"; - try - { - File.WriteAllText(@"D:\unity-integration-test-diag.txt", diagMessage); - } - catch - { - // If even D:\ root isn't writable, the crash dump is our only clue. - } - throw new IOException(diagMessage); - } + Logger.Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); #endif - Logger.Log("IntegrationTester, awake!"); Application.quitting += () => { From a44752126f10b5040f717d4df7616bd3a9bdcad8 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Mon, 30 Mar 2026 12:57:46 +0200 Subject: [PATCH 16/33] the builder does this now --- .../create-project.ps1 | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/test/Scripts.Integration.Test/create-project.ps1 b/test/Scripts.Integration.Test/create-project.ps1 index a748b9aab..6bafe4f60 100644 --- a/test/Scripts.Integration.Test/create-project.ps1 +++ b/test/Scripts.Integration.Test/create-project.ps1 @@ -45,27 +45,6 @@ $projectSettings = $projectSettings -replace "AndroidTargetArchitectures: ?[0-9] $projectSettings = $projectSettings -replace "iPhoneSdkVersion: ?[0-9]+", "iPhoneSdkVersion: 989" $projectSettings | Out-File $projectSettingsPath -# Set Xbox GameCore builds to Master (non-development) when the GameCore module is present. -# The build type is stored in Library/BuildProfiles and is only written to disk when the editor closes, -# so we patch the files directly after project creation while Unity is not running. -$buildProfilesPath = "$(GetNewProjectPath)/Library/BuildProfiles" -If (Test-Path -Path $buildProfilesPath) -{ - foreach ($profileFile in Get-ChildItem "$buildProfilesPath/PlatformProfile.*.asset") - { - $content = Get-Content $profileFile.FullName -Raw - If ($content -match "GameCoreXboxOne|GameCoreScarlett") - { - $updated = $content -replace "m_Development: 1", "m_Development: 0" - If ($updated -ne $content) - { - $updated | Out-File $profileFile.FullName -Encoding utf8 -NoNewline - Write-Detail "Set Master build profile: $($profileFile.Name)" - } - } - } -} - # Add Unity UI package to manifest.json if not already present # Creating a new project via command line doesn't include the Unity UI package by default while creating it via the Hub does. Write-Log "Checking Unity UI package in manifest.json..." From 4ec0d431c427edd6481c7b44247fa229dca2fba2 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Mon, 30 Mar 2026 15:18:28 +0200 Subject: [PATCH 17/33] loose build --- test/IntegrationTest/Integration.Tests.ps1 | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index f80f0c399..d10e110e8 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -247,14 +247,20 @@ BeforeAll { Connect-Device -Platform "Xbox" -Target $env:XBCONNECT_TARGET - # Xbox uses packaged .xvc flow — SENTRY_TEST_APP points to the package directory - $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" | Select-Object -First 1 - if (-not $xvcFile) { - throw "No .xvc package found in: $env:SENTRY_TEST_APP" + # Support both loose directory (no .xvc) and packaged directory (contains .xvc). + # Loose runs via xbrun: the app has full devkit filesystem access and can write to + # D:\Logs\ (required for capturing test output on non-development builds). + # Packaged runs via xbapp launch: the app is sandboxed and cannot write to D:\Logs\. + $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($xvcFile) { + Install-DeviceApp -Path $xvcFile.FullName + $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP + Write-Host "Using AUMID: $($script:ExecutablePath)" + } else { + # Loose flow: RunApplication mirrors the directory to the devkit and launches via xbrun + $script:ExecutablePath = $env:SENTRY_TEST_APP + Write-Host "Using loose executable directory: $($script:ExecutablePath)" } - Install-DeviceApp -Path $xvcFile.FullName - $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP - Write-Host "Using AUMID: $($script:ExecutablePath)" } "WebGL" { } From e43aa10f1a2a73a816f473210acba0fb21a4fe6b Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Mon, 30 Mar 2026 16:01:45 +0200 Subject: [PATCH 18/33] diagnostics --- test/IntegrationTest/Integration.Tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index d10e110e8..e605a7fa1 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -60,7 +60,9 @@ BeforeAll { $localFile = Join-Path $logLocalDir $logFileName $logContent = Get-Content $localFile -ErrorAction SilentlyContinue if (-not $logContent -or $logContent.Count -eq 0) { - throw "Xbox log file was empty or missing (D:\Logs\$logFileName)." + $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 @@ -148,6 +150,10 @@ BeforeAll { # 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") { + Write-Host "xbrun exit code: $($runResult.ExitCode)" + Write-Host "::group::xbrun raw output ($Action)" + $runResult.Output | ForEach-Object { Write-Host $_ } + Write-Host "::endgroup::" $runResult = Get-XboxLogOutput -RunResult $runResult -Action $Action } From 6c927fb1171ec44f346dca3532cf729425ae4ffd Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Tue, 31 Mar 2026 11:48:13 +0200 Subject: [PATCH 19/33] build package --- .../Editor/Builder.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 0f83a829f..49f6e965b 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -114,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"); @@ -133,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"); @@ -157,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"); @@ -175,12 +187,14 @@ 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"); 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"); @@ -188,6 +202,7 @@ public static void BuildXSXIL2CPPPlayer() BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/Xbox One")] public static void BuildXB1IL2CPPPlayer() { Debug.Log("Builder: Building Xbox One IL2CPP Player"); @@ -195,6 +210,7 @@ public static void BuildXB1IL2CPPPlayer() BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode); } + [MenuItem("Tools/Builder/PS5")] public static void BuildPS5IL2CPPPlayer() { Debug.Log("Builder: Building PS5 IL2CPP Player"); @@ -209,7 +225,9 @@ private static void SetXboxSubtargetToMaster() // 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)) { @@ -227,9 +245,12 @@ private static void SetXboxSubtargetToMaster() 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"); + Debug.Log($"Builder: Xbox Build Profile (BuildTarget {buildTarget}) set to Master, deploy method set to Package"); } + AssetDatabase.SaveAssets(); } @@ -250,7 +271,8 @@ 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/'"); } } From 11735f2961864af4ce9a5bb4812c6b7664964770 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Tue, 31 Mar 2026 11:59:21 +0200 Subject: [PATCH 20/33] simplified running --- test/IntegrationTest/Integration.Tests.ps1 | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index e605a7fa1..df5a21a0d 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -8,7 +8,7 @@ # SENTRY_AUTH_TOKEN: authentication token for Sentry API # # SENTRY_TEST_APP: path to the test app (APK, executable, .app bundle, WebGL build directory, -# or Xbox package directory containing .xvc) +# 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") @@ -253,20 +253,13 @@ BeforeAll { Connect-Device -Platform "Xbox" -Target $env:XBCONNECT_TARGET - # Support both loose directory (no .xvc) and packaged directory (contains .xvc). - # Loose runs via xbrun: the app has full devkit filesystem access and can write to - # D:\Logs\ (required for capturing test output on non-development builds). - # Packaged runs via xbapp launch: the app is sandboxed and cannot write to D:\Logs\. - $xvcFile = Get-ChildItem -Path $env:SENTRY_TEST_APP -Filter "*.xvc" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($xvcFile) { - Install-DeviceApp -Path $xvcFile.FullName - $script:ExecutablePath = Get-PackageAumid -PackagePath $env:SENTRY_TEST_APP - Write-Host "Using AUMID: $($script:ExecutablePath)" - } else { - # Loose flow: RunApplication mirrors the directory to the devkit and launches via xbrun - $script:ExecutablePath = $env:SENTRY_TEST_APP - Write-Host "Using loose executable directory: $($script:ExecutablePath)" + $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" { } From aa6cdf90b92cdb5cafdfb815b23a2f5da3c29641 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Tue, 31 Mar 2026 12:49:53 +0200 Subject: [PATCH 21/33] added ps5 and switch build configuration methods --- .../Editor/Builder.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 49f6e965b..02a89335f 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -191,6 +191,7 @@ public static void BuildWebGLPlayer() public static void BuildSwitchIL2CPPPlayer() { Debug.Log("Builder: Building Switch IL2CPP Player"); + SetSwitchCreateNspRomFile(); BuildIl2CPPPlayer(BuildTarget.Switch, BuildTargetGroup.Switch, BuildOptions.StrictMode); } @@ -214,13 +215,13 @@ public static void BuildXB1IL2CPPPlayer() public static void BuildPS5IL2CPPPlayer() { Debug.Log("Builder: Building PS5 IL2CPP Player"); + SetPS5BuildTypeToPackage(); BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode); } - // We'll likely extend this to also work with PS and Switch private static void SetXboxSubtargetToMaster() { - // The actual editor API to set this has bee deprecated: https://docs.unity3d.com/6000.3/Documentation/ScriptReference/XboxBuildSubtarget.html + // 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"); @@ -254,6 +255,64 @@ private static void SetXboxSubtargetToMaster() 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) From e3359ae7eb0f95c7ad3fac4104aa0f1ca473a8cf Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 31 Mar 2026 14:09:16 +0200 Subject: [PATCH 22/33] logger --- .../IntegrationOptionsConfiguration.cs | 1 + .../Scripts/IntegrationTester.cs | 3 - .../Scripts/Logger.cs | 59 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs index 00704c2a1..ff9416388 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationOptionsConfiguration.cs @@ -22,6 +22,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 e3015c913..95180a796 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -17,9 +17,6 @@ public class IntegrationTester : MonoBehaviour { private void Awake() { -#if UNITY_GAMECORE - Logger.Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); -#endif Logger.Log("IntegrationTester, awake!"); Application.quitting += () => { diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index aace32c3a..649aaad11 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -1,22 +1,39 @@ using System; using System.IO; +using Sentry; +using Sentry.Extensibility; using UnityEngine; /// -/// Shared log writer for integration tests. +/// 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) ends up in a -/// retrievable file. +/// 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 static class Logger +public class Logger : IDiagnosticLogger { + public static readonly Logger Instance = CreateInstance(); + + private static Logger CreateInstance() + { +#if UNITY_GAMECORE + Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); +#endif + return new Logger(); + } + private static StreamWriter s_writer; private static readonly object s_lock = new(); private static string s_logFilePath; + private SentryLevel _minLevel = SentryLevel.Debug; /// /// Opens the log file. Call once during initialization. @@ -98,4 +115,38 @@ private static void WriteToFile(string message) } } } + + // --- 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; + } + } } From 1f7099f4c2c1a05534e910f8d1083cb5d9e8bdbe Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Tue, 31 Mar 2026 14:29:36 +0200 Subject: [PATCH 23/33] editor --- test/Scripts.Integration.Test/Scripts/Logger.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index 649aaad11..205f04465 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -24,7 +24,9 @@ public class Logger : IDiagnosticLogger private static Logger CreateInstance() { -#if UNITY_GAMECORE +#if UNITY_EDITOR + +#elif UNITY_GAMECORE Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); #endif return new Logger(); @@ -40,7 +42,7 @@ private static Logger CreateInstance() /// 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. /// - public static void Open(string logFilePath) + private static void Open(string logFilePath) { lock (s_lock) { From 068fc332cf33304d76d5bb0b951074970aa6feaf Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 31 Mar 2026 14:47:24 +0200 Subject: [PATCH 24/33] iteration --- test/Scripts.Integration.Test/Scripts/Logger.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index 205f04465..4dbe0634f 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -20,23 +20,22 @@ /// 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_EDITOR - -#elif UNITY_GAMECORE +#if UNITY_GAMECORE && !UNITY_EDITOR Open(Path.Combine(@"D:\Logs", "UnityIntegrationTest.log")); #endif return new Logger(); } - private static StreamWriter s_writer; - private static readonly object s_lock = new(); - private static string s_logFilePath; - private SentryLevel _minLevel = SentryLevel.Debug; - /// /// Opens the log file. Call once during initialization. /// Throws if the file cannot be created — the caller should let the app crash From 05043ba35ce971a148a0e946bb816780dfb83db8 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 1 Apr 2026 12:39:30 +0200 Subject: [PATCH 25/33] Add Xbox devkit integration test runner Enable integration tests to be deployed and run on actual Xbox devkits: - Logger: file-based logger that writes to D:\Logs on Xbox, where Debug.Log output is suppressed in master (non-development) builds. Also implements IDiagnosticLogger for Sentry SDK diagnostics. - IntegrationTester: use Logger instead of Debug.Log throughout - Builder: add MenuItem attributes for manual builds, build configuration helpers for Xbox (master mode, package deploy), PS5 (package), and Switch (NSP ROM). Default buildPath when not specified. - Integration.Tests.ps1: Xbox platform support with devkit connect, XVC install, AUMID resolution, and log file retrieval via Copy-DeviceItem. Rename SENTRY_TEST_DSN to SENTRY_DSN. - integration-test.ps1: run Pester tests for Xbox instead of skipping --- test/IntegrationTest/Integration.Tests.ps1 | 79 ++++++++- .../Editor/Builder.cs | 130 ++++++++++++++- .../IntegrationOptionsConfiguration.cs | 5 + .../Scripts/IntegrationTester.cs | 29 ++-- .../Scripts/Logger.cs | 153 ++++++++++++++++++ .../Scripts/Logger.cs.meta | 11 ++ .../integration-test.ps1 | 7 +- 7 files changed, 388 insertions(+), 26 deletions(-) create mode 100644 test/Scripts.Integration.Test/Scripts/Logger.cs create mode 100644 test/Scripts.Integration.Test/Scripts/Logger.cs.meta 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$" { From 8312ea3ae677735a5a8dcc568e434fc40afe6c1b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 1 Apr 2026 14:58:14 +0200 Subject: [PATCH 26/33] fixed dsn env var name --- .github/workflows/test-run-android.yml | 2 +- .github/workflows/test-run-desktop.yml | 2 +- .github/workflows/test-run-ios.yml | 2 +- .github/workflows/test-run-webgl.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-run-android.yml b/.github/workflows/test-run-android.yml index 6f86b1216..63b19ae42 100644 --- a/.github/workflows/test-run-android.yml +++ b/.github/workflows/test-run-android.yml @@ -28,7 +28,7 @@ jobs: env: ARTIFACTS_PATH: samples/IntegrationTest/test-artifacts/ HOMEBREW_NO_INSTALL_CLEANUP: 1 - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} # Map the job outputs to step outputs outputs: diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index a9da553d2..7573bd83c 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -19,7 +19,7 @@ jobs: name: ${{ inputs.platform }} ${{ inputs.unity-version }} runs-on: ${{ inputs.platform == 'linux' && 'ubuntu-latest' || 'windows-latest' }} env: - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} steps: diff --git a/.github/workflows/test-run-ios.yml b/.github/workflows/test-run-ios.yml index e4851c455..6cc17a087 100644 --- a/.github/workflows/test-run-ios.yml +++ b/.github/workflows/test-run-ios.yml @@ -33,7 +33,7 @@ jobs: IOS_VERSION: ${{ inputs.ios-version }} INIT_TYPE: ${{ inputs.init-type }} ARTIFACTS_PATH: samples/IntegrationTest/test-artifacts/ - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} steps: diff --git a/.github/workflows/test-run-webgl.yml b/.github/workflows/test-run-webgl.yml index 78d82e806..49184f2f7 100644 --- a/.github/workflows/test-run-webgl.yml +++ b/.github/workflows/test-run-webgl.yml @@ -15,7 +15,7 @@ jobs: name: WebGL ${{ inputs.unity-version }} runs-on: ubuntu-latest env: - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} steps: From c160d1c4b523138daa9a38ba07359009d3de4d03 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 1 Apr 2026 15:05:23 +0200 Subject: [PATCH 27/33] handle storage requirement in postbuild --- .../Editor/Builder.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 02a89335f..6fa0dff5e 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Xml; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -403,3 +404,79 @@ public void OnPostprocessBuild(BuildReport report) } } } + +/// +/// Ensures Xbox builds have PersistentLocalStorage enabled in MicrosoftGame.config. +/// This is required for sentry-native to write its crash database and for integration +/// test logging to D:\Logs. +/// +public class XboxPersistentLocalStorage : IPostprocessBuildWithReport +{ + public int callbackOrder { get; } + + public void OnPostprocessBuild(BuildReport report) + { + if (report.summary.platform != BuildTarget.GameCoreXboxSeries + && report.summary.platform != BuildTarget.GameCoreXboxOne) + { + return; + } + + var configPath = Path.Combine(report.summary.outputPath, "MicrosoftGame.config"); + if (!File.Exists(configPath)) + { + Debug.LogError($"XboxPersistentLocalStorage: MicrosoftGame.config not found at '{configPath}'"); + return; + } + + var doc = new XmlDocument(); + doc.Load(configPath); + + var game = doc.DocumentElement; + if (game == null) + { + Debug.LogError("XboxPersistentLocalStorage: MicrosoftGame.config has no root element"); + return; + } + + // Find or create ExtendedAttributeList + var ns = game.NamespaceURI; + var nsMgr = new XmlNamespaceManager(doc.NameTable); + nsMgr.AddNamespace("ms", ns); + + var extList = game.SelectSingleNode("ms:ExtendedAttributeList", nsMgr) + ?? game.SelectSingleNode("ExtendedAttributeList"); + if (extList == null) + { + extList = doc.CreateElement("ExtendedAttributeList", ns); + game.AppendChild(extList); + } + + // Check if PersistentLocalStorage already exists + var found = false; + foreach (XmlNode child in extList.ChildNodes) + { + if (child is XmlElement el + && el.GetAttribute("Name") == "PersistentLocalStorage") + { + found = true; + break; + } + } + + if (!found) + { + var attr = doc.CreateElement("ExtendedAttribute", ns); + attr.SetAttribute("Name", "PersistentLocalStorage"); + attr.SetAttribute("Value", "true"); + extList.AppendChild(attr); + Debug.Log("XboxPersistentLocalStorage: Added PersistentLocalStorage to MicrosoftGame.config"); + } + else + { + Debug.Log("XboxPersistentLocalStorage: PersistentLocalStorage already present in MicrosoftGame.config"); + } + + doc.Save(configPath); + } +} From d55b89820f37ef30d15a380abaf60c01eb0978fd Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 13:16:39 +0200 Subject: [PATCH 28/33] Fix Xbox PersistentLocalStorage: use pre-build step on source config The post-build approach failed because Unity needs PersistentLocalStorage configured before the build starts. Changed to IPreprocessBuildWithReport targeting ProjectSettings/ScarlettGame.config and XboxOneGame.config with the correct XML structure (SizeMB + GrowableToMB). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Editor/Builder.cs | 64 ++++--------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 6fa0dff5e..ee10ce664 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -406,15 +406,14 @@ public void OnPostprocessBuild(BuildReport report) } /// -/// Ensures Xbox builds have PersistentLocalStorage enabled in MicrosoftGame.config. -/// This is required for sentry-native to write its crash database and for integration -/// test logging to D:\Logs. +/// Ensures Xbox builds have PersistentLocalStorage configured in the project's game config. +/// Required for sentry-native to write its crash database and for integration test logging. /// -public class XboxPersistentLocalStorage : IPostprocessBuildWithReport +public class XboxPersistentLocalStorage : IPreprocessBuildWithReport { public int callbackOrder { get; } - public void OnPostprocessBuild(BuildReport report) + public void OnPreprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.GameCoreXboxSeries && report.summary.platform != BuildTarget.GameCoreXboxOne) @@ -422,61 +421,24 @@ public void OnPostprocessBuild(BuildReport report) return; } - var configPath = Path.Combine(report.summary.outputPath, "MicrosoftGame.config"); - if (!File.Exists(configPath)) - { - Debug.LogError($"XboxPersistentLocalStorage: MicrosoftGame.config not found at '{configPath}'"); - return; - } + var configName = report.summary.platform == BuildTarget.GameCoreXboxSeries + ? "ScarlettGame.config" + : "XboxOneGame.config"; + var configPath = Path.Combine("ProjectSettings", configName); var doc = new XmlDocument(); doc.Load(configPath); var game = doc.DocumentElement; - if (game == null) - { - Debug.LogError("XboxPersistentLocalStorage: MicrosoftGame.config has no root element"); - return; - } - - // Find or create ExtendedAttributeList - var ns = game.NamespaceURI; - var nsMgr = new XmlNamespaceManager(doc.NameTable); - nsMgr.AddNamespace("ms", ns); - - var extList = game.SelectSingleNode("ms:ExtendedAttributeList", nsMgr) - ?? game.SelectSingleNode("ExtendedAttributeList"); - if (extList == null) + var pls = game["PersistentLocalStorage"] ?? doc.CreateElement("PersistentLocalStorage"); + if (pls.ParentNode == null) { - extList = doc.CreateElement("ExtendedAttributeList", ns); - game.AppendChild(extList); + game.AppendChild(pls); } - // Check if PersistentLocalStorage already exists - var found = false; - foreach (XmlNode child in extList.ChildNodes) - { - if (child is XmlElement el - && el.GetAttribute("Name") == "PersistentLocalStorage") - { - found = true; - break; - } - } - - if (!found) - { - var attr = doc.CreateElement("ExtendedAttribute", ns); - attr.SetAttribute("Name", "PersistentLocalStorage"); - attr.SetAttribute("Value", "true"); - extList.AppendChild(attr); - Debug.Log("XboxPersistentLocalStorage: Added PersistentLocalStorage to MicrosoftGame.config"); - } - else - { - Debug.Log("XboxPersistentLocalStorage: PersistentLocalStorage already present in MicrosoftGame.config"); - } + pls.InnerXml = "1122"; doc.Save(configPath); + Debug.Log($"XboxPersistentLocalStorage: Configured PersistentLocalStorage in {configName}"); } } From a3eebde8eda0bba0a0804d7ba2a10d3bc2a82ee9 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Thu, 2 Apr 2026 16:35:21 +0200 Subject: [PATCH 29/33] builder's final form --- .../Editor/AllowInsecureHttp.cs | 54 ++++ .../Editor/Builder.cs | 246 +++--------------- .../Editor/ConsoleBuildProfiles.cs | 118 +++++++++ .../Editor/XboxPersistentLocalStorage.cs | 44 ++++ 4 files changed, 249 insertions(+), 213 deletions(-) create mode 100644 test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs create mode 100644 test/Scripts.Integration.Test/Editor/ConsoleBuildProfiles.cs create mode 100644 test/Scripts.Integration.Test/Editor/XboxPersistentLocalStorage.cs diff --git a/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs b/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs new file mode 100644 index 000000000..4482aada7 --- /dev/null +++ b/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +public class AllowInsecureHttp : IPostprocessBuildWithReport, IPreprocessBuildWithReport +{ + public int callbackOrder { get; } + public void OnPreprocessBuild(BuildReport report) + { +#if UNITY_2022_1_OR_NEWER + PlayerSettings.insecureHttpOption = InsecureHttpOption.AlwaysAllowed; +#endif + } + + // The `allow insecure http always` options don't seem to work. This is why we modify the info.plist directly. + // Using reflection to get around the iOS module requirement on non-iOS platforms + public void OnPostprocessBuild(BuildReport report) + { + var pathToBuiltProject = report.summary.outputPath; + if (report.summary.platform == BuildTarget.iOS) + { + var plistPath = Path.Combine(pathToBuiltProject, "Info.plist"); + if (!File.Exists(plistPath)) + { + Debug.LogError("Failed to find the plist."); + return; + } + + var xcodeAssembly = Assembly.Load("UnityEditor.iOS.Extensions.Xcode"); + var plistType = xcodeAssembly.GetType("UnityEditor.iOS.Xcode.PlistDocument"); + var plistElementDictType = xcodeAssembly.GetType("UnityEditor.iOS.Xcode.PlistElementDict"); + + var plist = Activator.CreateInstance(plistType); + plistType.GetMethod("ReadFromString", BindingFlags.Public | BindingFlags.Instance) + ?.Invoke(plist, new object[] { File.ReadAllText(plistPath) }); + + var root = plistType.GetField("root", BindingFlags.Public | BindingFlags.Instance); + var allowDict = plistElementDictType.GetMethod("CreateDict", BindingFlags.Public | BindingFlags.Instance) + ?.Invoke(root?.GetValue(plist), new object[] { "NSAppTransportSecurity" }); + + plistElementDictType.GetMethod("SetBoolean", BindingFlags.Public | BindingFlags.Instance) + ?.Invoke(allowDict, new object[] { "NSAllowsArbitraryLoads", true }); + + var contents = (string)plistType.GetMethod("WriteToString", BindingFlags.Public | BindingFlags.Instance) + ?.Invoke(plist, null); + + File.WriteAllText(plistPath, contents); + } + } +} diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index ee10ce664..ee0191470 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; -using System.Xml; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -10,13 +8,14 @@ public class Builder { - public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, BuildOptions buildOptions) + public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, BuildOptions buildOptions, + string defaultBuildPath = "./Builds/") { Debug.Log("Builder: Starting to build"); Debug.Log("Builder: Parsing command line arguments"); var args = CommandLineArguments.Parse(); - ValidateArguments(args); + ValidateArguments(args, defaultBuildPath); Debug.Log($"Builder: Starting build. Output will be '{args["buildPath"]}'."); @@ -119,14 +118,16 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, public static void BuildWindowsIl2CPPPlayer() { Debug.Log("Builder: Building Windows IL2CPP Player"); - BuildIl2CPPPlayer(BuildTarget.StandaloneWindows64, BuildTargetGroup.Standalone, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.StandaloneWindows64, BuildTargetGroup.Standalone, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Windows/test.exe"); } [MenuItem("Tools/Builder/macOS")] public static void BuildMacIl2CPPPlayer() { Debug.Log("Builder: Building macOS IL2CPP Player"); - BuildIl2CPPPlayer(BuildTarget.StandaloneOSX, BuildTargetGroup.Standalone, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.StandaloneOSX, BuildTargetGroup.Standalone, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/macOS/test.app"); } [MenuItem("Tools/Builder/Linux")] @@ -137,7 +138,8 @@ public static void BuildLinuxIl2CPPPlayer() PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneLinux64, new[] { UnityEngine.Rendering.GraphicsDeviceType.OpenGLCore }); PlayerSettings.gpuSkinning = false; PlayerSettings.graphicsJobs = false; - BuildIl2CPPPlayer(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Linux/test"); } [MenuItem("Tools/Builder/Android")] @@ -163,21 +165,25 @@ public static void BuildAndroidIl2CPPPlayer() } #endif - BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Android/test.apk"); } + [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); + BuildIl2CPPPlayer(BuildTarget.Android, BuildTargetGroup.Android, BuildOptions.AcceptExternalModificationsToPlayer, + defaultBuildPath: "./Builds/AndroidProject/"); } [MenuItem("Tools/Builder/iOS")] public static void BuildIOSProject() { Debug.Log("Builder: Building iOS Project"); - BuildIl2CPPPlayer(BuildTarget.iOS, BuildTargetGroup.iOS, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.iOS, BuildTargetGroup.iOS, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/iOS/"); } [MenuItem("Tools/Builder/WebGL")] @@ -185,154 +191,53 @@ public static void BuildWebGLPlayer() { Debug.Log("Builder: Building WebGL Player"); PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli; - BuildIl2CPPPlayer(BuildTarget.WebGL, BuildTargetGroup.WebGL, BuildOptions.StrictMode); + BuildIl2CPPPlayer(BuildTarget.WebGL, BuildTargetGroup.WebGL, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/WebGL/"); } [MenuItem("Tools/Builder/Switch")] public static void BuildSwitchIL2CPPPlayer() { Debug.Log("Builder: Building Switch IL2CPP Player"); - SetSwitchCreateNspRomFile(); - BuildIl2CPPPlayer(BuildTarget.Switch, BuildTargetGroup.Switch, BuildOptions.StrictMode); + ConsoleBuildProfiles.SetSwitchCreateNspRomFile(); + BuildIl2CPPPlayer(BuildTarget.Switch, BuildTargetGroup.Switch, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Switch/test.nsp"); } [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); + ConsoleBuildProfiles.SetXboxSubtargetToMaster(); + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxSeries, BuildTargetGroup.GameCoreXboxSeries, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/GameCoreXboxSeries/test"); } [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); + ConsoleBuildProfiles.SetXboxSubtargetToMaster(); + BuildIl2CPPPlayer(BuildTarget.GameCoreXboxOne, BuildTargetGroup.GameCoreXboxOne, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/GameCoreXboxOne/test"); } [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; + ConsoleBuildProfiles.SetPS5BuildTypeToPackage(); + BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/PS5/test.pvx"); } - private static void ValidateArguments(Dictionary args) + private static void ValidateArguments(Dictionary args, string defaultBuildPath) { Debug.Log("Builder: Validating command line arguments"); if (!args.ContainsKey("buildPath") || string.IsNullOrWhiteSpace(args["buildPath"])) { - args["buildPath"] = "./Builds/"; - Debug.Log("Builder: No '-buildPath' provided, defaulting to './Builds/'"); + args["buildPath"] = defaultBuildPath; + Debug.Log($"Builder: No '-buildPath' provided, defaulting to '{defaultBuildPath}'"); } } @@ -357,88 +262,3 @@ private static void DisableProgressiveLightMapper() #endif } } - -public class AllowInsecureHttp : IPostprocessBuildWithReport, IPreprocessBuildWithReport -{ - public int callbackOrder { get; } - public void OnPreprocessBuild(BuildReport report) - { -#if UNITY_2022_1_OR_NEWER - PlayerSettings.insecureHttpOption = InsecureHttpOption.AlwaysAllowed; -#endif - } - - // The `allow insecure http always` options don't seem to work. This is why we modify the info.plist directly. - // Using reflection to get around the iOS module requirement on non-iOS platforms - public void OnPostprocessBuild(BuildReport report) - { - var pathToBuiltProject = report.summary.outputPath; - if (report.summary.platform == BuildTarget.iOS) - { - var plistPath = Path.Combine(pathToBuiltProject, "Info.plist"); - if (!File.Exists(plistPath)) - { - Debug.LogError("Failed to find the plist."); - return; - } - - var xcodeAssembly = Assembly.Load("UnityEditor.iOS.Extensions.Xcode"); - var plistType = xcodeAssembly.GetType("UnityEditor.iOS.Xcode.PlistDocument"); - var plistElementDictType = xcodeAssembly.GetType("UnityEditor.iOS.Xcode.PlistElementDict"); - - var plist = Activator.CreateInstance(plistType); - plistType.GetMethod("ReadFromString", BindingFlags.Public | BindingFlags.Instance) - ?.Invoke(plist, new object[] { File.ReadAllText(plistPath) }); - - var root = plistType.GetField("root", BindingFlags.Public | BindingFlags.Instance); - var allowDict = plistElementDictType.GetMethod("CreateDict", BindingFlags.Public | BindingFlags.Instance) - ?.Invoke(root?.GetValue(plist), new object[] { "NSAppTransportSecurity" }); - - plistElementDictType.GetMethod("SetBoolean", BindingFlags.Public | BindingFlags.Instance) - ?.Invoke(allowDict, new object[] { "NSAllowsArbitraryLoads", true }); - - var contents = (string)plistType.GetMethod("WriteToString", BindingFlags.Public | BindingFlags.Instance) - ?.Invoke(plist, null); - - File.WriteAllText(plistPath, contents); - } - } -} - -/// -/// Ensures Xbox builds have PersistentLocalStorage configured in the project's game config. -/// Required for sentry-native to write its crash database and for integration test logging. -/// -public class XboxPersistentLocalStorage : IPreprocessBuildWithReport -{ - public int callbackOrder { get; } - - public void OnPreprocessBuild(BuildReport report) - { - if (report.summary.platform != BuildTarget.GameCoreXboxSeries - && report.summary.platform != BuildTarget.GameCoreXboxOne) - { - return; - } - - var configName = report.summary.platform == BuildTarget.GameCoreXboxSeries - ? "ScarlettGame.config" - : "XboxOneGame.config"; - var configPath = Path.Combine("ProjectSettings", configName); - - var doc = new XmlDocument(); - doc.Load(configPath); - - var game = doc.DocumentElement; - var pls = game["PersistentLocalStorage"] ?? doc.CreateElement("PersistentLocalStorage"); - if (pls.ParentNode == null) - { - game.AppendChild(pls); - } - - pls.InnerXml = "1122"; - - doc.Save(configPath); - Debug.Log($"XboxPersistentLocalStorage: Configured PersistentLocalStorage in {configName}"); - } -} diff --git a/test/Scripts.Integration.Test/Editor/ConsoleBuildProfiles.cs b/test/Scripts.Integration.Test/Editor/ConsoleBuildProfiles.cs new file mode 100644 index 000000000..4b19a35e5 --- /dev/null +++ b/test/Scripts.Integration.Test/Editor/ConsoleBuildProfiles.cs @@ -0,0 +1,118 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +/// +/// Configures console platform build profiles via reflection. +/// The Unity Editor API for these settings is either deprecated or requires platform modules that +/// may not be installed. We access the internal BuildProfile assets directly instead. +/// +internal static class ConsoleBuildProfiles +{ + internal 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(); + } + + internal 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(); + } + + internal 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, true); + + 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; + } +} diff --git a/test/Scripts.Integration.Test/Editor/XboxPersistentLocalStorage.cs b/test/Scripts.Integration.Test/Editor/XboxPersistentLocalStorage.cs new file mode 100644 index 000000000..11c9e64d5 --- /dev/null +++ b/test/Scripts.Integration.Test/Editor/XboxPersistentLocalStorage.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Xml; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +/// +/// Ensures Xbox builds have PersistentLocalStorage configured in the project's game config. +/// Required for sentry-native to write its crash database and for integration test logging. +/// +public class XboxPersistentLocalStorage : IPreprocessBuildWithReport +{ + public int callbackOrder { get; } + + public void OnPreprocessBuild(BuildReport report) + { + if (report.summary.platform != BuildTarget.GameCoreXboxSeries + && report.summary.platform != BuildTarget.GameCoreXboxOne) + { + return; + } + + var configName = report.summary.platform == BuildTarget.GameCoreXboxSeries + ? "ScarlettGame.config" + : "XboxOneGame.config"; + var configPath = Path.Combine("ProjectSettings", configName); + + var doc = new XmlDocument(); + doc.Load(configPath); + + var game = doc.DocumentElement; + var pls = game["PersistentLocalStorage"] ?? doc.CreateElement("PersistentLocalStorage"); + if (pls.ParentNode == null) + { + game.AppendChild(pls); + } + + pls.InnerXml = "1122"; + + doc.Save(configPath); + Debug.Log($"XboxPersistentLocalStorage: Configured PersistentLocalStorage in {configName}"); + } +} From 56d341a05bf2e342b0554ab1c50eb045d210df2e Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Thu, 2 Apr 2026 16:48:24 +0200 Subject: [PATCH 30/33] fixed ps5 ending --- test/Scripts.Integration.Test/Editor/Builder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index ee0191470..ad14e0cb0 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -228,7 +228,7 @@ public static void BuildPS5IL2CPPPlayer() Debug.Log("Builder: Building PS5 IL2CPP Player"); ConsoleBuildProfiles.SetPS5BuildTypeToPackage(); BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode, - defaultBuildPath: "./Builds/PS5/test.pvx"); + defaultBuildPath: "./Builds/PS5/test.prx"); } private static void ValidateArguments(Dictionary args, string defaultBuildPath) From ecfbbc8ddc142611222a4f2803f0702d820a0337 Mon Sep 17 00:00:00 2001 From: bitsanfoxes Date: Thu, 2 Apr 2026 17:01:15 +0200 Subject: [PATCH 31/33] ps5 cleanup --- test/Scripts.Integration.Test/Editor/Builder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index ad14e0cb0..23629f858 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -228,7 +228,7 @@ public static void BuildPS5IL2CPPPlayer() Debug.Log("Builder: Building PS5 IL2CPP Player"); ConsoleBuildProfiles.SetPS5BuildTypeToPackage(); BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode, - defaultBuildPath: "./Builds/PS5/test.prx"); + defaultBuildPath: "./Builds/PS5/"); } private static void ValidateArguments(Dictionary args, string defaultBuildPath) From 6e0ca091982b06fb4d581159bec60be3dd11b0c6 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 17:25:09 +0200 Subject: [PATCH 32/33] bot feedback --- test/Scripts.Integration.Test/Scripts/IntegrationTester.cs | 2 +- test/Scripts.Integration.Test/Scripts/Logger.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs index 95180a796..3f8e05cb2 100644 --- a/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs +++ b/test/Scripts.Integration.Test/Scripts/IntegrationTester.cs @@ -181,7 +181,7 @@ private IEnumerator CrashCapture() Utils.ForceCrash(ForcedCrashCategory.AccessViolation); // Should not reach here - Logger.Log("ERROR: CRASH TEST: FAIL - unexpected code executed after crash"); + Logger.LogError("CRASH TEST: FAIL - unexpected code executed after crash"); Application.Quit(1); } diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index 4dbe0634f..42ffe7f11 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -134,7 +134,7 @@ void IDiagnosticLogger.Log(SentryLevel logLevel, string message, Exception excep formatted = $"{formatted} {exception}"; } - var line = $"[Sentry] ({logLevel}) {formatted}"; + var line = $"Sentry ({logLevel}) {formatted}"; switch (logLevel) { From 2a4bba7ad193a92bde85ff7385e5b57922b82a14 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 2 Apr 2026 17:37:05 +0200 Subject: [PATCH 33/33] removed unused path --- test/Scripts.Integration.Test/Scripts/Logger.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/Scripts.Integration.Test/Scripts/Logger.cs b/test/Scripts.Integration.Test/Scripts/Logger.cs index 42ffe7f11..bd098c59a 100644 --- a/test/Scripts.Integration.Test/Scripts/Logger.cs +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -61,14 +61,6 @@ private static void Open(string 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.