/* ┌──────────────────────────────────────────────────────────────────┐ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ │ Copyright (c) 2025 Ivan Murzak │ │ Licensed under the Apache License, Version 2.0. │ │ See the LICENSE file in the project root for more information. │ └──────────────────────────────────────────────────────────────────┘ */ #nullable enable using System; using System.Collections; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.Unity.MCP.TestFiles; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; namespace com.IvanMurzak.Unity.MCP.Editor.Tests { /// /// Tests for verifying that struct fields and properties can be partially populated /// without losing existing values for unspecified members. /// public class StructPopulationTests : BaseTest { [UnitySetUp] public override IEnumerator SetUp() => base.SetUp(); [UnityTearDown] public override IEnumerator TearDown() => base.TearDown(); [UnityTest] public IEnumerator Populate_Vector3Field_PartialUpdate_PreservesUnspecifiedValues() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values var initialVector = new Vector3(1f, 2f, 3f); comp.vector3Field = initialVector; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create a partial update that only modifies the 'x' field var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); var vector3Diff = new SerializedMember { name = "vector3Field" }; vector3Diff.fields = new SerializedMemberList(); vector3Diff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "x", type: typeof(float), value: 10f)); componentDiff.AddField(vector3Diff); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(10f, comp.vector3Field.x, 0.001f, "X should be updated to 10"); Assert.AreEqual(initialVector.y, comp.vector3Field.y, 0.001f, "Y should be preserved"); Assert.AreEqual(initialVector.z, comp.vector3Field.z, 0.001f, "Z should be preserved"); yield return null; } [UnityTest] public IEnumerator Populate_ColorField_PartialUpdate_PreservesUnspecifiedValues() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values var initialColor = new Color(0.1f, 0.2f, 0.3f, 0.4f); comp.colorField = initialColor; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create a partial update that only modifies 'r' and 'g' var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); var colorDiff = new SerializedMember { name = "colorField" }; colorDiff.fields = new SerializedMemberList(); colorDiff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "r", type: typeof(float), value: 1.0f)); colorDiff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "g", type: typeof(float), value: 0.5f)); componentDiff.AddField(colorDiff); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(1.0f, comp.colorField.r, 0.001f, "R should be updated to 1.0"); Assert.AreEqual(0.5f, comp.colorField.g, 0.001f, "G should be updated to 0.5"); Assert.AreEqual(initialColor.b, comp.colorField.b, 0.001f, "B should be preserved"); Assert.AreEqual(initialColor.a, comp.colorField.a, 0.001f, "A should be preserved"); yield return null; } [UnityTest] public IEnumerator Populate_CustomStruct_PartialUpdate_PreservesUnspecifiedValues() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values var initialStruct = new TestStruct { floatValue = 1.5f, intValue = 42, stringValue = "original", nestedVector = new Vector3(1f, 2f, 3f) }; comp.customStructField = initialStruct; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create a partial update that only modifies 'intValue' var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); var structDiff = new SerializedMember { name = "customStructField" }; structDiff.fields = new SerializedMemberList(); structDiff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "intValue", type: typeof(int), value: 100)); componentDiff.AddField(structDiff); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(100, comp.customStructField.intValue, "intValue should be updated to 100"); Assert.AreEqual(initialStruct.floatValue, comp.customStructField.floatValue, 0.001f, "floatValue should be preserved"); Assert.AreEqual(initialStruct.stringValue, comp.customStructField.stringValue, "stringValue should be preserved"); Assert.AreEqual(initialStruct.nestedVector, comp.customStructField.nestedVector, "nestedVector should be preserved"); yield return null; } [UnityTest] public IEnumerator Populate_NestedStruct_PartialUpdate_PreservesUnspecifiedValues() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values var initialStruct = new TestStruct { floatValue = 1.5f, intValue = 42, stringValue = "original", nestedVector = new Vector3(10f, 20f, 30f) }; comp.customStructField = initialStruct; var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create a partial update that only modifies 'nestedVector.x' var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); var vectorDiff = new SerializedMember { name = "nestedVector" }; vectorDiff.fields = new SerializedMemberList(); vectorDiff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "x", type: typeof(float), value: 99f)); var structDiff = new SerializedMember { name = "customStructField" }; structDiff.fields = new SerializedMemberList(); structDiff.fields.Add(vectorDiff); componentDiff.AddField(structDiff); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(99f, comp.customStructField.nestedVector.x, 0.001f, "nestedVector.x should be updated to 99"); Assert.AreEqual(initialStruct.nestedVector.y, comp.customStructField.nestedVector.y, 0.001f, "nestedVector.y should be preserved"); Assert.AreEqual(initialStruct.nestedVector.z, comp.customStructField.nestedVector.z, 0.001f, "nestedVector.z should be preserved"); Assert.AreEqual(initialStruct.intValue, comp.customStructField.intValue, "intValue should be preserved"); Assert.AreEqual(initialStruct.floatValue, comp.customStructField.floatValue, 0.001f, "floatValue should be preserved"); Assert.AreEqual(initialStruct.stringValue, comp.customStructField.stringValue, "stringValue should be preserved"); yield return null; } [UnityTest] public IEnumerator Populate_MultipleStructFields_PartialUpdate_PreservesUnspecifiedValues() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values comp.vector3Field = new Vector3(1f, 2f, 3f); comp.colorField = new Color(0.1f, 0.2f, 0.3f, 0.4f); comp.vector2Field = new Vector2(5f, 6f); var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create partial updates for multiple struct fields var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); // Update only vector3Field.y var vector3Diff = new SerializedMember { name = "vector3Field" }; vector3Diff.fields = new SerializedMemberList(); vector3Diff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "y", type: typeof(float), value: 100f)); componentDiff.AddField(vector3Diff); // Update only colorField.a var colorDiff = new SerializedMember { name = "colorField" }; colorDiff.fields = new SerializedMemberList(); colorDiff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "a", type: typeof(float), value: 1.0f)); componentDiff.AddField(colorDiff); // Update only vector2Field.x var vector2Diff = new SerializedMember { name = "vector2Field" }; vector2Diff.fields = new SerializedMemberList(); vector2Diff.fields.Add(SerializedMember.FromValue(reflector: reflector, name: "x", type: typeof(float), value: 50f)); componentDiff.AddField(vector2Diff); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); // Verify vector3Field Assert.AreEqual(1f, comp.vector3Field.x, 0.001f, "vector3Field.x should be preserved"); Assert.AreEqual(100f, comp.vector3Field.y, 0.001f, "vector3Field.y should be updated to 100"); Assert.AreEqual(3f, comp.vector3Field.z, 0.001f, "vector3Field.z should be preserved"); // Verify colorField Assert.AreEqual(0.1f, comp.colorField.r, 0.001f, "colorField.r should be preserved"); Assert.AreEqual(0.2f, comp.colorField.g, 0.001f, "colorField.g should be preserved"); Assert.AreEqual(0.3f, comp.colorField.b, 0.001f, "colorField.b should be preserved"); Assert.AreEqual(1.0f, comp.colorField.a, 0.001f, "colorField.a should be updated to 1.0"); // Verify vector2Field Assert.AreEqual(50f, comp.vector2Field.x, 0.001f, "vector2Field.x should be updated to 50"); Assert.AreEqual(6f, comp.vector2Field.y, 0.001f, "vector2Field.y should be preserved"); yield return null; } [UnityTest] public IEnumerator Populate_StructField_FullReplacement_Works() { // Arrange var go = new GameObject("TestStructPopulation"); var comp = go.AddComponent(); // Set initial values comp.vector3Field = new Vector3(1f, 2f, 3f); var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available."); // Create a full replacement (using valueJsonElement, not nested fields) var componentDiff = SerializedMember.FromValue( reflector: reflector, name: nameof(StructFieldPopulationTestScript), type: typeof(StructFieldPopulationTestScript), value: null ); // Full replacement - all fields are specified var newVector = new Vector3(100f, 200f, 300f); componentDiff.AddField(SerializedMember.FromValue(reflector: reflector, name: "vector3Field", type: typeof(Vector3), value: newVector)); // Act var objToModify = (object)comp; var logs = new Logs(); var success = reflector.TryModify( ref objToModify, data: componentDiff, logs: logs, logger: _logger); // Assert Assert.IsTrue(success, $"TryModify should succeed. Logs: {logs}"); Assert.AreEqual(newVector.x, comp.vector3Field.x, 0.001f, "vector3Field.x should be updated"); Assert.AreEqual(newVector.y, comp.vector3Field.y, 0.001f, "vector3Field.y should be updated"); Assert.AreEqual(newVector.z, comp.vector3Field.z, 0.001f, "vector3Field.z should be updated"); yield return null; } } }