Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/FastCloner.Internalization.Builder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ private static void PrintUsage()
Example:
MODERN=true;NET8_0_OR_GREATER=true
MODERN=true;SOMETHING=random_text
Default replacements:
MODERN_10=NET10_0_OR_GREATER
Special values:
true / false (recognized booleans)
Non-boolean values are used as direct token replacement in #if expressions.
Expand Down Expand Up @@ -389,6 +391,11 @@ internal enum PublicApiMode

internal sealed class BuildOptions
{
private static readonly IReadOnlyDictionary<string, string> DefaultPreprocessor = new Dictionary<string, string>(StringComparer.Ordinal)
{
["MODERN_10"] = "NET10_0_OR_GREATER"
};

public required string RootNamespace { get; init; }
public required string Output { get; init; }
public required string InputRoot { get; init; }
Expand Down Expand Up @@ -475,7 +482,7 @@ public HashSet<string> GetPreprocessorSymbols()

private static Dictionary<string, string> ParsePreprocessor(string raw)
{
Dictionary<string, string> map = new(StringComparer.Ordinal);
Dictionary<string, string> map = new(DefaultPreprocessor, StringComparer.Ordinal);
if (String.IsNullOrWhiteSpace(raw))
return map;

Expand Down
52 changes: 52 additions & 0 deletions src/FastCloner.Tests/CircularTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using FastCloner.Code;

namespace FastCloner.Tests;

[TestFixture(Low)]
Expand All @@ -21,6 +23,12 @@ public class C1
public C1 A { get; set; }
}

public sealed class SealedLoop
{
public int Value { get; set; }
public SealedLoop? Next { get; set; }
}

[Test]
public void SimpleLoop_Should_Be_Handled()
{
Expand Down Expand Up @@ -71,6 +79,19 @@ public void Object_Own_Loop_Should_Be_Handled()
Assert.That(cloned.A, Is.EqualTo(cloned));
}

[Test]
public void Sealed_Object_Own_Loop_Should_Be_Handled()
{
SealedLoop root = new SealedLoop { Value = 1 };
root.Next = root;

SealedLoop cloned = root.DeepClone();

Assert.That(cloned, Is.Not.SameAs(root));
Assert.That(cloned.Next, Is.SameAs(cloned));
Assert.That(cloned.Value, Is.EqualTo(1));
}

[Test]
public void Array_Of_Same_Objects_Should_Be_Cloned()
{
Expand All @@ -97,4 +118,35 @@ public void StructWrappedReferenceLoop_Should_Be_Handled()
Assert.That(cloned.W.Ref, Is.Not.Null);
Assert.That(cloned.W.Ref, Is.EqualTo(cloned));
}

[Test]
public void Internal_CloneClassInternal_Should_Reset_CallDepth_After_WorkList_Switch()
{
C1 root = new C1
{
F = 1,
A = new C1
{
F = 2,
A = new C1 { F = 3 }
}
};

int previousMaxRecursionDepth = FastCloner.MaxRecursionDepth;
FastCloner.MaxRecursionDepth = 1;
FastCloneState state = FastCloneState.Rent();
Comment thread
lofcz marked this conversation as resolved.
try
{
C1? clone = (C1?)FastClonerGenerator.CloneClassInternal(root, state);

Assert.That(clone, Is.Not.Null);
Assert.That(state.UseWorkList, Is.True);
Assert.That(state.CurrentDepth, Is.EqualTo(0));
}
finally
{
FastCloneState.Return(state);
FastCloner.MaxRecursionDepth = previousMaxRecursionDepth;
}
}
}
78 changes: 78 additions & 0 deletions src/FastCloner.Tests/DynamicMaxRecursionDepthTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
using FastCloner.Code;

namespace FastCloner.Tests;

[TestFixture]
public class DynamicMaxRecursionDepthTests
{
public sealed class ExactNode
{
public int Value { get; set; }
public ExactNode? Child { get; set; }
}

public class A
{
public List<B> Bs = [];
Expand Down Expand Up @@ -120,4 +128,74 @@ public void TestDeepObjectGraph_1500Levels_WithMRD1000()

Assert.That(level, Is.GreaterThan(maxRecursionDepth), $"Verified {level} levels");
}

[Test]
public void TestDeepExactObjectGraph_1500Levels_WithMRD1()
{
const int nestLevel = 1500;

ExactNode root = new ExactNode { Value = 0 };
ExactNode current = root;

for (int i = 1; i <= nestLevel; i++)
{
ExactNode next = new ExactNode { Value = i };
current.Child = next;
current = next;
}

FastCloner.MaxRecursionDepth = 1;
ExactNode? clone = FastCloner.DeepClone(root);

Assert.That(clone, Is.Not.Null);
Assert.That(clone, Is.Not.SameAs(root));

ExactNode? originalCurrent = root;
ExactNode? cloneCurrent = clone;
int verifiedLevels = 0;
while (originalCurrent is not null && cloneCurrent is not null)
{
Assert.That(cloneCurrent, Is.Not.SameAs(originalCurrent), $"Node at level {verifiedLevels} should be cloned");
Assert.That(cloneCurrent.Value, Is.EqualTo(originalCurrent.Value), $"Value at level {verifiedLevels} should match");

originalCurrent = originalCurrent.Child;
cloneCurrent = cloneCurrent.Child;
verifiedLevels++;
}

Assert.That(verifiedLevels, Is.EqualTo(nestLevel + 1));
Assert.That(cloneCurrent, Is.Null);
Assert.That(originalCurrent, Is.Null);
}

[Test]
public void Internal_CloneClassInternalExact_Should_Reset_CallDepth_After_WorkList_Switch()
{
ExactNode root = new ExactNode
{
Value = 1,
Child = new ExactNode
{
Value = 2,
Child = new ExactNode { Value = 3 }
}
};

int previousMaxRecursionDepth = FastCloner.MaxRecursionDepth;
FastCloner.MaxRecursionDepth = 1;
FastCloneState state = FastCloneState.Rent();
try
{
ExactNode? clone = FastClonerGenerator.CloneClassInternalExact(root, state);

Assert.That(clone, Is.Not.Null);
Assert.That(state.UseWorkList, Is.True);
Assert.That(state.CurrentDepth, Is.EqualTo(0));
}
finally
{
FastCloneState.Return(state);
FastCloner.MaxRecursionDepth = previousMaxRecursionDepth;
}
}
}
18 changes: 18 additions & 0 deletions src/FastCloner.Tests/TypeBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@ public void SetTypeBehavior_Ignore_RootObjectOfIgnoredType_ReturnsNull()
// Assert
Assert.That(cloned, Is.Null, "Cloning an object of an globally ignored type should return null.");
}

[Test]
public void Internal_CloneClassInternalExact_IgnoreTypeBehavior_ReturnsNull()
{
ClassToBeIgnored original = new ClassToBeIgnored { Data = "Important Data" };
FastCloner.SetTypeBehavior<ClassToBeIgnored>(CloneBehavior.Ignore);

FastCloneState state = FastCloneState.Rent();
try
{
ClassToBeIgnored? cloned = FastClonerGenerator.CloneClassInternalExact(original, state);
Assert.That(cloned, Is.Null);
}
finally
{
FastCloneState.Return(state);
}
}

[Test]
public void SetTypeBehavior_Ignore_ForPrimitiveIntProperty_SetsToDefault()
Expand Down
1 change: 1 addition & 0 deletions src/FastCloner/Code/FastCloneState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private FastCloneState(bool trackReferences = true)
private int workCount;
public bool UseWorkList { get; set; }
private int callDepth;
internal int CurrentDepth => callDepth;
private readonly Type?[] metadataTypes = new Type?[MetadataCacheSize];
private readonly FastClonerCache.TypeCloneMetadata[] metadataValues = new FastClonerCache.TypeCloneMetadata[MetadataCacheSize];
private int metadataWriteIndex;
Expand Down
Loading
Loading