Skip to content

Commit 3c5cbfc

Browse files
committed
fix on-fly swap from recursive to iterative cloning
1 parent 2accee6 commit 3c5cbfc

File tree

3 files changed

+139
-4
lines changed

3 files changed

+139
-4
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
namespace FastCloner.Tests;
2+
3+
[TestFixture]
4+
public class DynamicMaxRecursionDepthTests
5+
{
6+
public class A
7+
{
8+
public List<B> Bs = [];
9+
public B? Child;
10+
}
11+
12+
public class B
13+
{
14+
public A? Child;
15+
}
16+
17+
[OneTimeSetUp]
18+
public void BaseOneTimeSetUp()
19+
{
20+
21+
}
22+
23+
[OneTimeTearDown]
24+
public void BaseOneTimeTearDown()
25+
{
26+
FastCloner.MaxRecursionDepth = 1000;
27+
}
28+
29+
[Test]
30+
public void TestDynamicMaxRecursionDepth()
31+
{
32+
// Arrange
33+
A orig = new A();
34+
orig.Bs.Add(new B());
35+
36+
// Act & Assert: MRD = 1
37+
{
38+
FastCloner.MaxRecursionDepth = 1;
39+
A? clone = FastCloner.DeepClone(orig);
40+
AssertClone(clone);
41+
}
42+
43+
// Act & Assert: MRD = 2
44+
{
45+
FastCloner.MaxRecursionDepth = 2;
46+
A? clone = FastCloner.DeepClone(orig);
47+
AssertClone(clone);
48+
}
49+
50+
// Act & Assert: MRD = 3
51+
{
52+
FastCloner.MaxRecursionDepth = 3;
53+
A? clone = FastCloner.DeepClone(orig);
54+
AssertClone(clone);
55+
}
56+
57+
void AssertClone(A? clone)
58+
{
59+
using (Assert.EnterMultipleScope())
60+
{
61+
Assert.That((orig == clone).Dump(), Is.EqualTo("false"), $"Orig should not be the same as clone for depth {FastCloner.MaxRecursionDepth}");
62+
Assert.That((orig.Bs == clone.Bs).Dump(), Is.EqualTo("false"), $"Orig.Bs should not be the same as clone.Bs for depth {FastCloner.MaxRecursionDepth}");
63+
Assert.That((orig.Bs.First() == clone.Bs.First()).Dump(), Is.EqualTo("false"), $"Orig.Bs.First() should not be the same as clone.Bs.First() for depth {FastCloner.MaxRecursionDepth}");
64+
}
65+
}
66+
}
67+
68+
[Test]
69+
public void TestDeepObjectGraph_1500Levels_WithMRD1000()
70+
{
71+
// Arrange
72+
const int nestLevel = 1500;
73+
const int maxRecursionDepth = 1000;
74+
75+
A root = new A();
76+
A currentA = root;
77+
78+
for (int i = 0; i < nestLevel; i++)
79+
{
80+
B newB = new B();
81+
currentA.Child = newB;
82+
83+
if (i < nestLevel - 1)
84+
{
85+
A newA = new A();
86+
newB.Child = newA;
87+
currentA = newA;
88+
}
89+
}
90+
91+
// Act
92+
FastCloner.MaxRecursionDepth = maxRecursionDepth;
93+
A? clone = FastCloner.DeepClone(root);
94+
95+
// Assert
96+
Assert.That(clone, Is.Not.Null);
97+
Assert.That((root == clone).Dump(), Is.EqualTo("false"));
98+
99+
A? origA = root;
100+
A? clonedA = clone;
101+
int level = 0;
102+
103+
while (origA != null && clonedA != null)
104+
{
105+
Assert.That((origA == clonedA).Dump(), Is.EqualTo("false"), $"A at level {level} should be different objects");
106+
107+
if (origA.Child != null && clonedA.Child != null)
108+
{
109+
Assert.That((origA.Child == clonedA.Child).Dump(), Is.EqualTo("false"), $"B at level {level} should be different objects");
110+
111+
origA = origA.Child.Child;
112+
clonedA = clonedA.Child.Child;
113+
level++;
114+
}
115+
else
116+
{
117+
break;
118+
}
119+
}
120+
121+
Assert.That(level, Is.GreaterThan(maxRecursionDepth), $"Verified {level} levels");
122+
}
123+
}

FastCloner.Tests/FastCloner.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PrivateAssets>all</PrivateAssets>
2626
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2727
</PackageReference>
28+
<PackageReference Include="ObjectDumper.NET" Version="4.3.4-pre" />
2829
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
2930
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
3031
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />

FastCloner/Code/FastClonerGenerator.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ internal static class FastClonerGenerator
8181
UseWorkList = TypeHasDirectSelfReference(rootType)
8282
};
8383

84+
object result;
85+
8486
if (!state.UseWorkList)
8587
{
8688
try
@@ -91,12 +93,18 @@ internal static class FastClonerGenerator
9193
{
9294
state.DecrementDepth();
9395
state.UseWorkList = true;
96+
result = cloner(obj, state);
9497
}
9598
else
9699
{
97-
object resultNormal = cloner(obj, state);
100+
result = cloner(obj, state);
98101
state.DecrementDepth();
99-
return resultNormal;
102+
103+
// if UseWorkList was set during recursive cloning, process the worklist
104+
if (!state.UseWorkList)
105+
{
106+
return result;
107+
}
100108
}
101109
}
102110
catch
@@ -105,8 +113,11 @@ internal static class FastClonerGenerator
105113
throw;
106114
}
107115
}
108-
109-
object result = cloner(obj, state);
116+
else
117+
{
118+
result = cloner(obj, state);
119+
}
120+
110121
while (state.TryPop(out object from, out object to, out Type type))
111122
{
112123
// boxed value types - MemberwiseClone already created a value copy.

0 commit comments

Comments
 (0)