From 8e6918e488d24e5bfe7807f9f50ea99584c2a4ec Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Tue, 26 Nov 2024 10:05:27 +0100 Subject: [PATCH] #35: Add methods for deep and shallow copying DynamicObj, add tests --- src/DynamicObj/DynamicObj.fs | 35 ++++++ .../DynamicObject.Tests.fsproj | 1 + tests/DynamicObject.Tests/DynamicObjs.fs | 116 ++++++++++++++++++ tests/DynamicObject.Tests/TestUtils.fs | 34 +++++ 4 files changed, 186 insertions(+) create mode 100644 tests/DynamicObject.Tests/TestUtils.fs diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 8c4b6c0..d66879c 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -245,6 +245,36 @@ type DynamicObj() = ) /// + /// + /// + /// The target object to copy dynamic members to + /// Whether existing properties on the target object will be overwritten + member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) = + let overWrite = defaultArg overWrite false + let rec tryDeepCopyObj (o:obj) = + match o with + | :? DynamicObj as dyn -> + let newDyn = DynamicObj() + for kv in (dyn.GetProperties(false)) do + newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value) + box newDyn + | :? array as dyns -> + box [|for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj|] + | :? list as dyns -> + box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj] + | :? ResizeArray as dyns -> + box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj])) + //| :? System.ICloneable as clonable -> clonable.Clone() + | _ -> o + + this.GetProperties(false) + |> Seq.iter (fun kv -> + match target.TryGetPropertyHelper kv.Key with + | Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value) + | Some _ -> () + | None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value) + ) + /// /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties). /// /// Note that this function does not attempt to do any deep copying. @@ -256,6 +286,11 @@ type DynamicObj() = this.ShallowCopyDynamicPropertiesTo(target) target + member this.DeepCopyDynamicProperties() = + let target = DynamicObj() + this.DeepCopyDynamicPropertiesTo(target) + target + #if !FABLE_COMPILER // Some necessary overrides for methods inherited from System.Dynamic.DynamicObject() // diff --git a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj index 53a9307..9dd8ae7 100644 --- a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj +++ b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj @@ -8,6 +8,7 @@ + diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs index 46aa66a..221c8d0 100644 --- a/tests/DynamicObject.Tests/DynamicObjs.fs +++ b/tests/DynamicObject.Tests/DynamicObjs.fs @@ -3,6 +3,7 @@ open System open Fable.Pyxpecto open DynamicObj +open TestUtils let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [ testCase "NonExisting" <| fun _ -> @@ -485,6 +486,120 @@ let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" Expect.equal a b "copied value was not mutated via reference" ] +let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ + + let constructClone (props: seq) = + let original = DynamicObj() + props + |> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue)) + let clone = original.DeepCopyDynamicProperties() + original, clone + + let bulkMutate (props: seq) (dyn: DynamicObj) = + props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue)) + + testList "DynamicObj" [ + testList "Cloneable dynamic properties" [ + testCase "primitives" <| fun _ -> + let originalProps = [ + "int", box 1 + "float", box 1.0 + "bool", box true + "string", box "hello" + "char", box 'a' + "byte", box (byte 1) + "sbyte", box (sbyte -1) + "int16", box (int16 -1) + "uint16", box (uint16 1) + "int32", box (int32 -1) + "uint32", box (uint32 1u) + "int64", box (int64 -1L) + "uint64", box (uint64 1UL) + "single", box (single 1.0f) + "decimal", box (decimal 1M) + ] + let original, clone = constructClone originalProps + let mutatedProps = [ + "int", box 2 + "float", box 2.0 + "bool", box false + "string", box "bye" + "char", box 'b' + "byte", box (byte 2) + "sbyte", box (sbyte -2) + "int16", box (int16 -2) + "uint16", box (uint16 2) + "int32", box (int32 -2) + "uint32", box (uint32 2u) + "int64", box (int64 -2L) + "uint64", box (uint64 2UL) + "single", box (single 2.0f) + "decimal", box (decimal 2M) + ] + bulkMutate mutatedProps original + Expect.notEqual original clone "Original and clone should not be equal after mutating primitive props on original" + Expect.sequenceEqual (original.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) mutatedProps "Original should have mutated properties" + Expect.sequenceEqual (clone.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) originalProps "Clone should have original properties" + testCase "DynamicObj" <| fun _ -> + let inner = DynamicObj() |> DynObj.withProperty "inner int" 2 + let original, clone = constructClone ["dyn", inner] + inner.SetProperty("inner int", 1) + Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original" + Expect.equal (original |> DynObj.getNestedPropAs ["dyn";"inner int"]) 1 "Original should have mutated properties" + Expect.equal (clone |> DynObj.getNestedPropAs ["dyn";"inner int"]) 2 "Clone should have original properties" + testCase "Nested DynamicObj" <| fun _ -> + let first_level = DynamicObj() |> DynObj.withProperty "lvl1" 1 + let second_level = DynamicObj() |> DynObj.withProperty "lvl2" 2 + first_level.SetProperty("second_level", second_level) + let original, clone = constructClone ["first_level", first_level] + second_level.SetProperty("lvl2", -1) + Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original" + Expect.equal (original |> DynObj.getNestedPropAs ["first_level";"second_level";"lvl2"]) -1 "Original should have mutated properties" + Expect.equal (clone |> DynObj.getNestedPropAs ["first_level";"second_level";"lvl2"]) 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, clone = constructClone ["arr", box arr] + item1.SetProperty("item", -1) + item2.SetProperty("item", -1) + item3.SetProperty("item", -1) + let originalProp = original |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) + let clonedProp = clone |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) + Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original" + Expect.sequenceEqual originalProp [|-1; -1; -1|] "Original should have mutated properties" + Expect.equal clonedProp [|1; 2; 3|] "Clone should have original properties" + testCase " + () + testCase "DynamicObj ResizeArray" <| fun _ -> + () + ] + testList "Un-Cloneable dynamic properties" [ + testCase "Class with mutable fields is reference equal" <| fun _ -> + () + ] + ] + testList "Derived class" [ + testList "Cloneable dynamic properties" [ + testCase "primitives" <| fun _ -> + () + testCase "DynamicObj" <| fun _ -> + () + testCase "DynamicObj array" <| fun _ -> + () + testCase " + () + testCase "DynamicObj ResizeArray" <| fun _ -> + () + ] + testList "Un-Cloneable dynamic properties" [ + testCase "Class with mutable fields is reference equal" <| fun _ -> + () + ] + ] +] + let tests_Equals = testList "Equals" [ testCase "Same Object" <| fun _ -> let a = DynamicObj() @@ -583,6 +698,7 @@ let main = testList "DynamicObj (Class)" [ tests_GetProperties tests_ShallowCopyDynamicPropertiesTo tests_ShallowCopyDynamicProperties + tests_DeepCopyDynamicProperties tests_Equals tests_GetHashCode ] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs new file mode 100644 index 0000000..caef957 --- /dev/null +++ b/tests/DynamicObject.Tests/TestUtils.fs @@ -0,0 +1,34 @@ +module TestUtils + +open DynamicObj + +let firstDiff s1 s2 = + let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None)) + let s2 = Seq.append (Seq.map Some s2) (Seq.initInfinite (fun _ -> None)) + Seq.mapi2 (fun i s p -> i,s,p) s1 s2 + |> Seq.find (function |_,Some s,Some p when s=p -> false |_-> true) + +module DynObj = + let inline getNestedPropAs<'T> (propTree: seq) (dyn: DynamicObj) = + let props = propTree |> Seq.toList + let rec getProp (dyn: DynamicObj) (props: string list) : 'T= + match props with + | p::[] -> (dyn.GetPropertyValue(p)) |> unbox<'T> + | p::ps -> getProp (dyn.GetPropertyValue(p) |> unbox) ps + | _ -> failwith "Empty property list" + getProp dyn props + +module Expect = + /// Expects the `actual` sequence to equal the `expected` one. + let sequenceEqual actual expected message = + match firstDiff actual expected with + | _,None,None -> () + | i,Some a, Some e -> + failwithf "%s. Sequence does not match at position %i. Expected item: %O, but got %O." + message i e a + | i,None,Some e -> + failwithf "%s. Sequence actual shorter than expected, at pos %i for expected item %O." + message i e + | i,Some a,None -> + failwithf "%s. Sequence actual longer than expected, at pos %i found item %O." + message i a \ No newline at end of file