Skip to content

Commit

Permalink
Add deep copy tests for derived classes
Browse files Browse the repository at this point in the history
  • Loading branch information
kMutagene committed Dec 13, 2024
1 parent e56e88c commit b518f9a
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 10 deletions.
33 changes: 31 additions & 2 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,9 @@ type DynamicObj() =
)

/// <summary>
/// Attempts to perform a deep copy of the DynamicObj.
/// Attempts to deep copy the properties of the DynamicObj onto the target.
///
/// On the deep copy, as many properties as possible are re-instantiated as new objects, meaning the
/// As many properties as possible are re-instantiated as new objects, meaning the
/// copy has as little reference equal properties as possible.
///
/// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
Expand Down Expand Up @@ -331,6 +331,35 @@ type DynamicObj() =
this.ShallowCopyDynamicPropertiesTo(target)
target

/// <summary>

Check warning on line 334 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is invalid: unknown parameter 'target'

Check warning on line 334 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is invalid: unknown parameter 'overWrite'

Check warning on line 334 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is invalid: unknown parameter 'target'

Check warning on line 334 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is invalid: unknown parameter 'overWrite'
/// Attempts to perform a deep copy of the DynamicObj.
///
/// On the deep copy, as many properties as possible are re-instantiated as new objects, meaning the
/// copy has as little reference equal properties as possible.
///
/// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
/// their type is not known on runtime and the contructors of the types are not known.
///
/// The following cases are handled (in this precedence):
///
/// - Basic F# types (int, float, bool, string, char, byte, sbyte, int16, uint16, int32, uint32, int64, uint64, single, decimal)
///
/// - array&lt;DynamicObj&gt;, list&lt;DynamicObj&gt;, ResizeArray&lt;DynamicObj&gt;: These collections of DynamicObj are copied as a new collection with recursively deep copied elements.
///
/// - System.ICloneable: If the property implements ICloneable, the Clone() method is called on the property.
///
/// - DynamicObj (and derived classes): properties that are themselves DynamicObj instances are deep copied recursively.
/// if a derived class has static properties (e.g. instance properties), these will be copied as dynamic properties on the new instance.
///
/// Note on Classes that inherit from DynamicObj:
///
/// Classes that inherit from DynamicObj will match the `DynamicObj` typecheck if they do not implement ICloneable.
/// The deep coopied instances will be cast to DynamicObj with static/instance properties AND dynamic properties all set as dynamic properties.
/// It should be possible to 'recover' the original type by checking if the needed properties exist as dynamic properties,
/// and then passing them to the class constructor if needed.
/// </summary>
/// <param name="target">The target object to copy dynamic members to</param>
/// <param name="overWrite">Whether existing properties on the target object will be overwritten</param>
member this.DeepCopyDynamicProperties() =
let target = DynamicObj()
this.DeepCopyDynamicPropertiesTo(target)
Expand Down
89 changes: 82 additions & 7 deletions tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
Expect.referenceEqual originalProp clonedProp "Original and cloned property should be reference equal"
]
]
testList "Derived class" [
testList "Derived class implementing ICloneable" [
testList "Cloneable dynamic properties" [
testCase "primitives" <| fun _ ->
()
Expand All @@ -147,7 +147,23 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
()
]
]
testList "Derived class implementing ICloneable" [
testList "Derived class" [
testList "SpecialCases" [
testCase "copy has instance prop as dynamic prop" <| fun _ ->
let original = DerivedClass(stat = "stat", dyn = "dyn")
let clone = original.DeepCopyDynamicProperties()
let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
Expect.containsAll clonedProps ["stat","stat"] "Clone should have static prop from derived class as dynamic prop"
testCase "mutable instance prop is reference equal on clone" <| fun _ ->
let original = DerivedClass(stat = "stat", dyn = "dyn")
let mut = MutableClass("initial")
original.SetProperty("mutable", mut)
let clone = original.DeepCopyDynamicProperties()
mut.stat <- "mutated"
let originalProp = original |> DynObj.getNestedPropAs<MutableClass>["mutable"]
let clonedProp = clone |> DynObj.getNestedPropAs<MutableClass> ["mutable"]
Expect.equal originalProp clonedProp "Original and clone should be equal after mutating mutable field on original"
]
testList "Cloneable dynamic properties" [
testCase "primitives" <| fun _ ->
let originalProps = [
Expand Down Expand Up @@ -216,18 +232,77 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
"Clone should have original and static properties"
Expect.isTrue (original.GetType() = typeof<DerivedClass>) "Original is of type DerivedClass"
Expect.isTrue (clone.GetType() = typeof<DynamicObj>) "Clone is of type DynamicObj"

testCase "DynamicObj" <| fun _ ->
()
let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("inner", inner)
let clone = original.DeepCopyDynamicProperties()
inner.SetProperty("inner int", 1)

Expect.equal (original |> DynObj.getNestedPropAs<int> ["inner";"inner int"]) 1 "Original should have mutated properties"
Expect.equal (clone |> DynObj.getNestedPropAs<int> ["inner";"inner int"]) 2 "Clone should have original properties"

testCase "DynamicObj array" <| fun _ ->
()
let item1 = DynamicObj() |> DynObj.withProperty "item" 1
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let arr = [|item1; item2; item3|]
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("arr", arr)
let clone = original.DeepCopyDynamicProperties()
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
let originalProp = original |> DynObj.getNestedPropAs<DynamicObj array> ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
let clonedProp = clone |> DynObj.getNestedPropAs<DynamicObj array> ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
Expect.sequenceEqual originalProp [|-1; -1; -1|] "Original should have mutated properties"
Expect.sequenceEqual clonedProp [|1; 2; 3|] "Clone should have original properties"

testCase "DynamicObj list" <| fun _ ->
()
let item1 = DynamicObj() |> DynObj.withProperty "item" 1
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let l = [item1; item2; item3]
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("list", l)
let clone = original.DeepCopyDynamicProperties()
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
let originalProp = original |> DynObj.getNestedPropAs<DynamicObj list> ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
let clonedProp = clone |> DynObj.getNestedPropAs<DynamicObj list> ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
Expect.sequenceEqual originalProp [-1; -1; -1] "Original should have mutated properties"
Expect.sequenceEqual clonedProp [1; 2; 3] "Clone should have original properties"

testCase "DynamicObj ResizeArray" <| fun _ ->
()
let item1 = DynamicObj() |> DynObj.withProperty "item" 1
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let r = ResizeArray([item1; item2; item3])
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("resizeArr", r)
let clone = original.DeepCopyDynamicProperties()
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
let originalProp = original |> DynObj.getNestedPropAs<ResizeArray<DynamicObj>> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn) |> ResizeArray
let clonedProp = clone |> DynObj.getNestedPropAs<ResizeArray<DynamicObj>> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn) |> ResizeArray
Expect.sequenceEqual originalProp (ResizeArray[-1; -1; -1]) "Original should have mutated properties"
Expect.sequenceEqual clonedProp (ResizeArray[1; 2; 3]) "Clone should have original properties"
]
testList "Un-Cloneable dynamic properties" [
testCase "Class with mutable fields is reference equal" <| fun _ ->
()
let item = MutableClass("initial")
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("item", item)
let clone = original.DeepCopyDynamicProperties()
item.stat <- "mutated"
let originalProp = original |> DynObj.getNestedPropAs<MutableClass>["item"]
let clonedProp = clone |> DynObj.getNestedPropAs<MutableClass> ["item"]
Expect.equal originalProp.stat "mutated" "Original property has mutated value"
Expect.equal clonedProp.stat "mutated" "Cloned property has mutated value"
Expect.referenceEqual originalProp clonedProp "Original and cloned property should be reference equal"
]
]
]
2 changes: 1 addition & 1 deletion tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let tests_GetProperties = testList "GetProperties" [
System.Collections.Generic.KeyValuePair("b", box 2)
]
Expect.sequenceEqual properties expected "Should have all properties"
testCase "returns static instance members when wanted" <| fun _ ->
testCase "returns static instance members of derived class when wanted" <| fun _ ->
let a = DerivedClass(stat = "stat", dyn = "dyn")
let properties = a.GetProperties(true) |> List.ofSeq |> List.sortBy (fun kv -> kv.Key)
let expected =
Expand Down

0 comments on commit b518f9a

Please sign in to comment.