using System; using System.IO; using Newtonsoft.Json.Linq; using NUnit.Framework; using Unity.Pipeline.Editor.Authoring; using Unity.Pipeline.Editor.Commands.Capture; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace Unity.Pipeline.Tests.Editor.Capture { /// /// Tests for the visual-feedback commands (CLI-199), exercised directly and via PipelineClient. /// Render tests are GPU-gated: under batchmode/headless the graphics device is /// and the tests self-ignore rather than fail. /// public class CaptureCommandsTests { private const string CameraName = "CLI199_Cam"; private const string SaveFolder = "Assets/CLI199_CaptureTests"; // PNG file signature: 0x89 'P' 'N' 'G' 0x0D 0x0A 0x1A 0x0A. private static readonly byte[] PngSignature = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; private GameObject m_CameraObject; [SetUp] public void SetUp() { m_CameraObject = new GameObject(CameraName); m_CameraObject.AddComponent(); } [TearDown] public void TearDown() { if (m_CameraObject != null) UnityEngine.Object.DestroyImmediate(m_CameraObject); if (AssetDatabase.IsValidFolder(SaveFolder)) AssetDatabase.DeleteAsset(SaveFolder); } private static string AbsolutePath(string projectRelative) => Path.Combine(ProjectPaths.ProjectRoot, projectRelative); private static bool IsHeadless => SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null; private static void AssertPngSignature(byte[] bytes) { Assert.GreaterOrEqual(bytes.Length, PngSignature.Length, "PNG payload too short to contain a signature"); for (var i = 0; i < PngSignature.Length; i++) Assert.AreEqual(PngSignature[i], bytes[i], $"PNG signature mismatch at byte {i}"); } #region Direct [Test] public void CaptureGameView_NamedCamera_ReturnsPng() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var result = CaptureCommands.CaptureGameView(64, 64, CameraName); Assert.IsNotNull(result, "Capture should return a result"); Assert.IsNotEmpty(result.Base64, "Base64 payload should be non-empty"); Assert.AreEqual(64, result.Width, "Width should match the requested size"); Assert.AreEqual(64, result.Height, "Height should match the requested size"); Assert.AreEqual("png", result.Encoding); Assert.AreEqual($"camera:{CameraName}", result.Source); Assert.IsNull(result.SavedPath, "No savePath was requested"); var bytes = Convert.FromBase64String(result.Base64); AssertPngSignature(bytes); Assert.AreEqual(bytes.Length, result.Bytes, "Reported byte length should match decoded payload"); } [Test] public void CaptureSceneView_ReturnsPng() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var sv = SceneView.lastActiveSceneView; if (sv == null) Assert.Ignore("No Scene View"); var result = CaptureCommands.CaptureSceneView(64, 64); Assert.IsNotNull(result, "Capture should return a result"); Assert.IsNotEmpty(result.Base64, "Base64 payload should be non-empty"); Assert.AreEqual("sceneView", result.Source); var bytes = Convert.FromBase64String(result.Base64); AssertPngSignature(bytes); } [Test] public void CaptureGameView_SavePath_OmitsBase64_AndWritesPng() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var result = CaptureCommands.CaptureGameView(64, 64, CameraName, $"{SaveFolder}/path_only.png"); Assert.IsNull(result.Base64, "save_path without include_inline_image should omit the base64 payload"); Assert.IsNotNull(result.SavedPath, "SavedPath should be set"); var fileBytes = File.ReadAllBytes(AbsolutePath(result.SavedPath)); AssertPngSignature(fileBytes); Assert.AreEqual(fileBytes.Length, result.Bytes, "Reported byte length should match the file"); } [Test] public void CaptureGameView_SavePath_IncludeImage_ReturnsFileAndBase64() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var result = CaptureCommands.CaptureGameView(64, 64, CameraName, $"{SaveFolder}/both.png", includeInlineImage: true); Assert.IsNotEmpty(result.Base64, "include_inline_image=true should return the base64 payload"); Assert.IsNotNull(result.SavedPath, "SavedPath should be set"); AssertPngSignature(Convert.FromBase64String(result.Base64)); AssertPngSignature(File.ReadAllBytes(AbsolutePath(result.SavedPath))); } [Test] public void CaptureGameView_MaxResolution_ClampsInlineRender() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); // No save_path: the inline image is the only artifact, so the cap applies to the render. var result = CaptureCommands.CaptureGameView(128, 64, CameraName, maxResolution: 32); Assert.AreEqual(32, result.Width, "Long edge should be clamped to max_resolution"); Assert.AreEqual(16, result.Height, "Short edge should keep the aspect ratio"); AssertPngSignature(Convert.FromBase64String(result.Base64)); } [Test] public void CaptureGameView_SavePath_MaxResolution_DownscalesInlineOnly() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var result = CaptureCommands.CaptureGameView(128, 64, CameraName, $"{SaveFolder}/full.png", includeInlineImage: true, maxResolution: 32); Assert.AreEqual(128, result.Width, "The saved file keeps the requested resolution"); Assert.AreEqual(32, result.InlineWidth, "The inline copy is downscaled to max_resolution"); Assert.AreEqual(16, result.InlineHeight, "The inline copy keeps the aspect ratio"); AssertPngSignature(Convert.FromBase64String(result.Base64)); AssertPngSignature(File.ReadAllBytes(AbsolutePath(result.SavedPath))); } [Test] public void CaptureSceneView_SavePath_OmitsBase64_AndWritesPng() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); var sv = SceneView.lastActiveSceneView; if (sv == null) Assert.Ignore("No Scene View"); var result = CaptureCommands.CaptureSceneView(64, 64, $"{SaveFolder}/scene_path_only.png"); Assert.IsNull(result.Base64, "save_path without include_inline_image should omit the base64 payload"); Assert.IsNotNull(result.SavedPath, "SavedPath should be set"); AssertPngSignature(File.ReadAllBytes(AbsolutePath(result.SavedPath))); } #endregion #region ViaClient [Test] public void CaptureGameView_ViaClient_Succeeds() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); using (var server = new PipelineTestServer()) { var response = server.Execute("capture_game_view", new { width = 64, height = 64, camera = CameraName }); Assert.IsTrue(response.IsSuccess, $"capture_game_view should succeed: {response.Error}"); Assert.IsTrue(response.HasValidJson, "Response should have valid JSON"); Assert.IsTrue(response.JsonResponse.ContainsKey("result"), "Should have result field"); } } [Test] public void CaptureGameView_ViaClient_SavePath_SerializedResultOmitsBase64Key() { if (IsHeadless) Assert.Ignore("No GPU in batchmode"); using (var server = new PipelineTestServer()) { var response = server.Execute("capture_game_view", new { width = 64, height = 64, camera = CameraName, save_path = $"{SaveFolder}/via_client.png" }); Assert.IsTrue(response.IsSuccess, $"capture_game_view should succeed: {response.Error}"); var result = (JObject)response.JsonResponse["result"]; Assert.IsFalse(result.ContainsKey("base64"), "A path-only result must not carry a base64 key on the wire (AUTHAPI-8)"); Assert.IsNotNull(result["savedPath"]?.ToString(), "savedPath should be present"); } } #endregion } }