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: 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/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 1e776efab..23629f858 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -9,18 +8,20 @@ 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"]}'."); // 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,23 @@ 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); + 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")] public static void BuildLinuxIl2CPPPlayer() { Debug.Log("Builder: Building Linux IL2CPP Player"); @@ -130,8 +138,11 @@ 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")] public static void BuildAndroidIl2CPPPlayer() { Debug.Log("Builder: Building Android IL2CPP Player"); @@ -154,56 +165,79 @@ 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")] 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"); - 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"); - 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"); - 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"); - BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode); + ConsoleBuildProfiles.SetPS5BuildTypeToPackage(); + BuildIl2CPPPlayer(BuildTarget.PS5, BuildTargetGroup.PS5, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/PS5/"); } - 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"])) { - throw new Exception("No valid '-buildPath' has been provided."); + args["buildPath"] = defaultBuildPath; + Debug.Log($"Builder: No '-buildPath' provided, defaulting to '{defaultBuildPath}'"); } } @@ -228,50 +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); - } - } -} 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}"); + } +} 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..3f8e05cb2 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.LogError("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..bd098c59a --- /dev/null +++ b/test/Scripts.Integration.Test/Scripts/Logger.cs @@ -0,0 +1,145 @@ +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; + } + } + + /// + /// 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$" {