using System; using System.Collections.Generic; namespace SceneTests.Components; /// /// Tests for Component.Clone.cs - how component members are transferred when a /// GameObject is cloned: value types are copied directly (via the compiled /// member-copy delegates), reference types are deep-copied through JSON, /// ICloneable types use their own Clone(), and references to components inside /// the cloned hierarchy are remapped onto the clones. /// [TestClass] [DoNotParallelize] public class ComponentCloneTest : SceneTest { /// /// Value-type properties and fields (and strings) are copied from the /// original onto the clone, and the copy goes in the right direction - the /// original is never written to. /// [TestMethod] public void ValueMembersAreCopiedToClone() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Number = 42; comp.Offset = new Vector3( 1, 2, 3 ); comp.Title = "hello"; var clone = go.Clone(); var cloneComp = clone.GetComponent(); Assert.IsNotNull( cloneComp ); Assert.AreEqual( 42, cloneComp.Number ); Assert.AreEqual( new Vector3( 1, 2, 3 ), cloneComp.Offset ); Assert.AreEqual( "hello", cloneComp.Title ); cloneComp.Number = 7; Assert.AreEqual( 42, comp.Number ); } /// /// Mutable reference-type properties like lists must be deep-copied, so /// mutating the clone's list doesn't change the original. /// [TestMethod] public void ListPropertyIsDeepCopied() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Values = new List { 1, 2, 3 }; var clone = go.Clone(); var cloneComp = clone.GetComponent(); Assert.AreNotSame( comp.Values, cloneComp.Values ); CollectionAssert.AreEqual( comp.Values, cloneComp.Values ); cloneComp.Values.Add( 4 ); Assert.AreEqual( 3, comp.Values.Count ); } /// /// A member type that declares its own Clone() (ICloneable) is cloned via /// that method, producing an equal but distinct instance. /// [TestMethod] public void ICloneableMemberIsClonedViaCloneMethod() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Bag = new CloneableBag { Value = 5 }; var clone = go.Clone(); var cloneComp = clone.GetComponent(); Assert.IsNotNull( cloneComp.Bag ); Assert.AreNotSame( comp.Bag, cloneComp.Bag ); Assert.AreEqual( 5, cloneComp.Bag.Value ); } /// /// Null reference members stay null on the clone - cloning must not /// fabricate instances. /// [TestMethod] public void NullMembersStayNull() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Title = null; comp.Bag = null; var clone = go.Clone(); var cloneComp = clone.GetComponent(); Assert.IsNull( cloneComp.Title ); Assert.IsNull( cloneComp.Bag ); } /// /// A component property referencing a component inside the cloned hierarchy /// is remapped to the cloned instance - here a self-reference must point at /// the clone itself, never back at the original. /// [TestMethod] public void ComponentReferenceIsRemappedToClone() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var comp = go.Components.Create(); comp.Buddy = comp; var clone = go.Clone(); var cloneComp = clone.GetComponent(); Assert.AreSame( cloneComp, cloneComp.Buddy ); Assert.AreNotSame( comp, cloneComp.Buddy ); } } /// /// Component whose members cover the different cloning strategies: value-type /// property and field, string, mutable list, ICloneable class and a component /// reference. /// public class CloneDataComponent : Component { [Property] public int Number { get; set; } [Property] public Vector3 Offset; [Property] public string Title { get; set; } [Property] public List Values { get; set; } = new(); [Property] public CloneableBag Bag { get; set; } [Property] public CloneDataComponent Buddy { get; set; } } /// /// Plain class implementing ICloneable so the clone system uses its Clone() /// instead of a JSON round trip. /// public class CloneableBag : ICloneable { public int Value { get; set; } /// /// Returns a copy of this bag carrying the same value. /// public object Clone() => new CloneableBag { Value = Value }; }