diff --git a/DeepClone.Test/EnumerableCloneTests.cs b/DeepClone.Test/EnumerableCloneTests.cs new file mode 100644 index 0000000..73714d7 --- /dev/null +++ b/DeepClone.Test/EnumerableCloneTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.DeepClone.Test; + +/// +/// Tests for deep cloning plain sequences and stacks through their extension methods. +/// +[TestClass] +public class EnumerableCloneTests +{ +#pragma warning disable CA1851 // Enumerating the clone more than once is what these tests check + /// + /// Tests that edits to the items of a cloned sequence survive a second enumeration. + /// + [TestMethod] + public void Enumerable_DeepClone_ShouldKeepEditsAcrossEnumerations() + { + // Arrange + List original = [new() { Id = 1, Name = "Item1" }]; + + // Act + IEnumerable clone = original.DeepClone(); + foreach (SimpleObject item in clone) + { + item.Name = "Edited"; + } + + // Assert + Assert.AreEqual("Edited", clone.First().Name); + Assert.AreSame(clone.First(), clone.First()); + Assert.AreEqual("Item1", original[0].Name); + } + + /// + /// Tests that a cloned sequence does not change when the source changes afterwards. + /// + [TestMethod] + public void Enumerable_DeepClone_ShouldNotTrackSourceChanges() + { + // Arrange + List original = + [ + new() { Id = 1, Name = "Item1" }, + new() { Id = 2, Name = "Item2" }, + ]; + + // Act + IEnumerable clone = original.DeepClone(); + original.Clear(); + + // Assert + Assert.AreEqual(2, clone.Count()); + } +#pragma warning restore CA1851 + + /// + /// Tests that deep cloning a null sequence throws ArgumentNullException. + /// + [TestMethod] + public void Enumerable_DeepClone_NullSource_ShouldThrow() + { + IEnumerable source = null!; + Assert.ThrowsExactly(() => source.DeepClone()); + } + + /// + /// Tests that deep cloning a null stack throws ArgumentNullException. + /// + [TestMethod] + public void Stack_DeepClone_NullSource_ShouldThrow() + { + Stack source = null!; + Assert.ThrowsExactly(() => source.DeepClone()); + } +} diff --git a/DeepClone/DeepCloneContainerExtensions.cs b/DeepClone/DeepCloneContainerExtensions.cs index 11d5f80..3d46d5f 100644 --- a/DeepClone/DeepCloneContainerExtensions.cs +++ b/DeepClone/DeepCloneContainerExtensions.cs @@ -104,8 +104,9 @@ public static void DeepCloneFrom(this IDictionary de /// A new collection containing deep clones of the original items if they implement IDeepCloneable, /// otherwise containing the original items. /// - /// This method returns an IEnumerable sequence of cloned items. To get a specific collection type, - /// you'll need to convert the result (for example using ToList() or ToArray()). + /// The items are cloned when this method is called, so the result is a snapshot: it does not change + /// when the source changes, and enumerating it again yields the same cloned instances. To get a + /// specific collection type, convert the result (for example using ToList() or ToArray()). /// /// Example usage: /// @@ -113,8 +114,13 @@ public static void DeepCloneFrom(this IDictionary de /// var clonedArray = originalArray.DeepClone().ToArray(); /// /// - public static IEnumerable DeepClone(this IEnumerable source) => - source.Select(DeepClone); + /// Thrown if source is null. + public static IEnumerable DeepClone(this IEnumerable source) + { + Ensure.NotNull(source); + + return [.. source.Select(DeepClone)]; + } /// /// Deep clones a dictionary. @@ -314,6 +320,11 @@ public static SortedSet DeepClone(this SortedSet source) /// var clonedStack = originalStack.DeepClone(); /// /// - public static Stack DeepClone(this Stack source) => - new(source.Reverse().Select(DeepClone)); + /// Thrown if source is null. + public static Stack DeepClone(this Stack source) + { + Ensure.NotNull(source); + + return new(source.Reverse().Select(DeepClone)); + } }