From 6e98be7dbd85ff30334c4a12b27ff8dfd8acc817 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 20 Mar 2026 11:13:24 +0100 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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