From 9cd7d469b41ea4c9c79445607338bc5cc5862415 Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Mon, 25 Nov 2024 08:21:10 +0100 Subject: [PATCH 1/6] #35 Do not throw expections when copying dynamic properties --- src/DynamicObj/DynamicObj.fs | 20 +++++++---- src/DynamicObj/Playground.fsx | 21 +++++++++--- tests/DynamicObject.Tests/DynamicObjs.fs | 43 +++++++++++++++--------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 0b65f24..8c4b6c0 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -224,30 +224,36 @@ type DynamicObj() = |> Seq.map (fun kv -> kv.Key) /// - /// Copies all dynamic members of the DynamicObj to the target DynamicObj. + /// Copies all dynamic members of the source DynamicObj to the target DynamicObj. /// - /// If overWrite is set to true, existing properties on the target object will be overwritten. + /// Note that this function does not attempt to do any deep copying. + /// The dynamic properties of the source will be copied as references to the target. + /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target. /// - /// Note that this method will not perform nested checks, e.g. if a property is a DynamicObj itself, it will not be copied recursively. + /// If overWrite is set to true, existing properties on the target object will be overwritten. /// /// The target object to copy dynamic members to /// Whether existing properties on the target object will be overwritten - member this.CopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) = + member this.ShallowCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) = let overWrite = defaultArg overWrite false this.GetProperties(false) |> Seq.iter (fun kv -> match target.TryGetPropertyHelper kv.Key with | Some pi when overWrite -> pi.SetValue target kv.Value - | Some _ -> failwith $"Property \"{kv.Key}\" already exists on target object and overWrite was not set to true." + | Some _ -> () | None -> target.SetProperty(kv.Key,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. + /// The dynamic properties of the source will be copied as references to the target. + /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target. /// - member this.CopyDynamicProperties() = + member this.ShallowCopyDynamicProperties() = let target = DynamicObj() - this.CopyDynamicPropertiesTo(target) + this.ShallowCopyDynamicPropertiesTo(target) target #if !FABLE_COMPILER diff --git a/src/DynamicObj/Playground.fsx b/src/DynamicObj/Playground.fsx index 620bbf8..1c5f40a 100644 --- a/src/DynamicObj/Playground.fsx +++ b/src/DynamicObj/Playground.fsx @@ -2,6 +2,10 @@ #r "nuget: Fable.Core" #r "nuget: Fable.Pyxpecto" +#load "./HashCodes.fs" +#load "./PropertyHelper.fs" +#load "./FablePy.fs" +#load "./FableJS.fs" #load "./ReflectionUtils.fs" #load "./DynamicObj.fs" #load "./DynObj.fs" @@ -9,11 +13,18 @@ open Fable.Pyxpecto open DynamicObj +type T(dyn:string, stat:string) as this= + inherit DynamicObj() -let a = DynamicObj () -a.SetValue("aaa", 5) -let b = DynamicObj () -b.SetValue("aaa", 5) + do + this.SetProperty("Dyn", dyn) + member this.Stat = stat -a.GetProperties(true) \ No newline at end of file +let first = T("dyn1", "stat1") +let second = T("dyn2", "stat2") + +let _ = second.ShallowCopyDynamicPropertiesTo(first) + +first |> DynObj.print +second |> DynObj.print \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs index 168f384..46aa66a 100644 --- a/tests/DynamicObject.Tests/DynamicObjs.fs +++ b/tests/DynamicObject.Tests/DynamicObjs.fs @@ -433,43 +433,56 @@ let tests_GetProperties = testList "GetProperties" [ Expect.equal properties expected "Should have all properties" ] -let tests_CopyDynamicPropertiesTo = testList "CopyDynamicPropertiesTo" [ +let tests_ShallowCopyDynamicPropertiesTo = testList "ShallowCopyDynamicPropertiesTo" [ testCase "ExistingObject" <| fun _ -> let a = DynamicObj() a.SetProperty("a", 1) a.SetProperty("b", 2) let b = DynamicObj() b.SetProperty("c", 3) - a.CopyDynamicPropertiesTo(b) + a.ShallowCopyDynamicPropertiesTo(b) Expect.equal (b.GetPropertyValue("a")) 1 "Value a should be copied" Expect.equal (b.GetPropertyValue("b")) 2 "Value b should be copied" Expect.equal (b.GetPropertyValue("c")) 3 "Value c should be unaffected" - testCase "NoOverwrite throws" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = DynamicObj() - b.SetProperty("a", 3) - let f = fun () -> a.CopyDynamicPropertiesTo(b) - Expect.throws f "Should throw because property exists" - testCase "Overwrite" <| fun _ -> let a = DynamicObj() a.SetProperty("a", 1) let b = DynamicObj() b.SetProperty("a", 3) Expect.notEqual a b "Values should not be equal before copying" - a.CopyDynamicPropertiesTo(b, true) + a.ShallowCopyDynamicPropertiesTo(b, true) Expect.equal a b "Values should be equal" + + testCase "copies are only references" <| fun _ -> + let a = DynamicObj() + let inner = DynamicObj() + inner.SetProperty("inner", 1) + a.SetProperty("nested", inner) + let b = DynamicObj() + a.ShallowCopyDynamicPropertiesTo(b) + Expect.equal a b "Value should be copied" + inner.SetProperty("another", 2) + Expect.equal a b "copied value was not mutated via reference" ] -let tests_CopyDynamicProperties = testList "CopyDynamicProperties" [ +let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" [ testCase "NewObject" <| fun _ -> let a = DynamicObj() a.SetProperty("a", 1) a.SetProperty("b", 2) - let b = a.CopyDynamicProperties() + let b = a.ShallowCopyDynamicProperties() Expect.equal a b "Values should be equal" + + testCase "copies are only references" <| fun _ -> + let a = DynamicObj() + let inner = DynamicObj() + inner.SetProperty("inner", 1) + a.SetProperty("nested", inner) + let b = a.ShallowCopyDynamicProperties() + Expect.equal a b "Value should be copied" + inner.SetProperty("another", 2) + Expect.equal a b "copied value was not mutated via reference" ] let tests_Equals = testList "Equals" [ @@ -568,8 +581,8 @@ let main = testList "DynamicObj (Class)" [ tests_RemoveProperty tests_GetPropertyHelpers tests_GetProperties - tests_CopyDynamicPropertiesTo - tests_CopyDynamicProperties + tests_ShallowCopyDynamicPropertiesTo + tests_ShallowCopyDynamicProperties tests_Equals tests_GetHashCode ] \ No newline at end of file From 8e6918e488d24e5bfe7807f9f50ea99584c2a4ec Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Tue, 26 Nov 2024 10:05:27 +0100 Subject: [PATCH 2/6] #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 From e157a0038b62633807158b68c0d901968181be85 Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Mon, 9 Dec 2024 09:33:53 +0100 Subject: [PATCH 3/6] Fix python transpilation errors when typematching decimal or interface --- DynamicObj.sln | 10 ++-------- src/DynamicObj/DynamicObj.fs | 20 +++++++++++++++++++- tests/DynamicObject.Tests/DynamicObjs.fs | 23 ++++++++++++++++++++--- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/DynamicObj.sln b/DynamicObj.sln index b60139b..89c4a52 100644 --- a/DynamicObj.sln +++ b/DynamicObj.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{39AA72A1 ProjectSection(SolutionItems) = preProject build.cmd = build.cmd build.sh = build.sh + global.json = global.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".ci", ".ci", "{F82A6F26-517C-4D5E-BD4F-BFC45B5867FE}" @@ -20,14 +21,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".proj", ".proj", "{C3CF2F15-81C7-4C11-889E-5FCA2C8A981D}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore - build.cmd = build.cmd - build.sh = build.sh - .config\dotnet-tools.json = .config\dotnet-tools.json - global.json = global.json - key.snk = key.snk LICENSE = LICENSE - package.json = package.json - pyproject.toml = pyproject.toml README.md = README.md RELEASE_NOTES.md = RELEASE_NOTES.md EndProjectSection @@ -37,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8 docs\index.fsx = docs\index.fsx EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A-3A42-4E46-B233-B64F5C22524B}" EndProject diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index d66879c..4c168ba 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -253,8 +253,21 @@ type DynamicObj() = let overWrite = defaultArg overWrite false let rec tryDeepCopyObj (o:obj) = match o with + | :? int | :? float | :? bool + | :? string | :? char | :? byte + | :? sbyte | :? int16 | :? uint16 + | :? int32 | :? uint32 | :? int64 + | :? uint64 | :? single + -> o + + #if !FABLE_COMPILER_PYTHON + // https://github.com/fable-compiler/Fable/issues/3971 + | :? decimal -> o + #endif + | :? DynamicObj as dyn -> let newDyn = DynamicObj() + // might want to keep instance props as dynamic props on copy for kv in (dyn.GetProperties(false)) do newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value) box newDyn @@ -264,7 +277,12 @@ type DynamicObj() = 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() + + #if !FABLE_COMPILER_PYTHON + // https://github.com/fable-compiler/Fable/issues/3972 + | :? System.ICloneable as clonable -> clonable.Clone() + #endif + | _ -> o this.GetProperties(false) diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs index 221c8d0..c646c43 100644 --- a/tests/DynamicObject.Tests/DynamicObjs.fs +++ b/tests/DynamicObject.Tests/DynamicObjs.fs @@ -486,6 +486,22 @@ let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" Expect.equal a b "copied value was not mutated via reference" ] +type DerivedClass(stat: string, dyn: string) as this = + inherit DynamicObj() + do + this.SetProperty("dyn", dyn) + member this.Stat = stat + +type DerivedClassCloneable(stat: string, dyn: string) as this = + inherit DynamicObj() + do + this.SetProperty("dyn", dyn) + member this.Stat = stat + interface ICloneable with + member this.Clone() = + let dyn = this.GetPropertyValue("dyn") |> unbox + DerivedClassCloneable(stat, dyn) + let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let constructClone (props: seq) = @@ -498,6 +514,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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 _ -> @@ -534,7 +551,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ "int64", box (int64 -2L) "uint64", box (uint64 2UL) "single", box (single 2.0f) - "decimal", box (decimal 2M) + "decimal", box (decimal 2M) ] bulkMutate mutatedProps original Expect.notEqual original clone "Original and clone should not be equal after mutating primitive props on original" @@ -570,7 +587,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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 list" <| fun _ -> () testCase "DynamicObj ResizeArray" <| fun _ -> () @@ -588,7 +605,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ () testCase "DynamicObj array" <| fun _ -> () - testCase " + testCase "DynamicObj list" <| fun _ -> () testCase "DynamicObj ResizeArray" <| fun _ -> () From e56e88c011cdc5bc2c504551eef5417abd072bbe Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Fri, 13 Dec 2024 15:11:48 +0100 Subject: [PATCH 4/6] Restructure and add more tests --- src/DynamicObj/DynamicObj.fs | 41 +- .../DynamicObject.Tests.fsproj | 17 +- tests/DynamicObject.Tests/DynamicObjs.fs | 721 ------------------ .../DynamicObjs/DeepCopyDynamicProperties.fs | 233 ++++++ .../DynamicObject.Tests/DynamicObjs/Equals.fs | 38 + .../DynamicObjs/GetHashcode.fs | 53 ++ .../DynamicObjs/GetProperties.fs | 28 + .../DynamicObjs/GetPropertyHelpers.fs | 15 + .../DynamicObjs/GetPropertyValue.fs | 62 ++ tests/DynamicObject.Tests/DynamicObjs/Main.fs | 31 + .../DynamicObjs/RemoveProperty.fs | 95 +++ .../DynamicObjs/SetProperty.fs | 78 ++ .../ShallowCopyDynamicProperties.fs | 24 + .../ShallowCopyDynamicPropertiesTo.fs | 38 + .../TryGetDynamicPropertyHelper.fs | 25 + .../DynamicObjs/TryGetPropertyHelper.fs | 29 + .../DynamicObjs/TryGetPropertyValue.fs | 63 ++ .../DynamicObjs/TryGetStaticPropertyHelper.fs | 25 + .../DynamicObjs/TryGetTypedPropertyValue.fs | 69 ++ tests/DynamicObject.Tests/Main.fs | 2 +- tests/DynamicObject.Tests/TestUtils.fs | 42 +- 21 files changed, 998 insertions(+), 731 deletions(-) delete mode 100644 tests/DynamicObject.Tests/DynamicObjs.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/Equals.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/Main.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs create mode 100644 tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 4c168ba..16bd912 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -245,7 +245,31 @@ type DynamicObj() = ) /// + /// 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<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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. /// /// The target object to copy dynamic members to /// Whether existing properties on the target object will be overwritten @@ -253,6 +277,9 @@ type DynamicObj() = let overWrite = defaultArg overWrite false let rec tryDeepCopyObj (o:obj) = match o with + + // might be that we do not need this case, however if we remove it, some types will match the + // ICloneable case in transpiled code, which we'd like to prevent, so well keep it for now. | :? int | :? float | :? bool | :? string | :? char | :? byte | :? sbyte | :? int16 | :? uint16 @@ -265,12 +292,6 @@ type DynamicObj() = | :? decimal -> o #endif - | :? DynamicObj as dyn -> - let newDyn = DynamicObj() - // might want to keep instance props as dynamic props on copy - 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 -> @@ -283,9 +304,15 @@ type DynamicObj() = | :? System.ICloneable as clonable -> clonable.Clone() #endif + | :? DynamicObj as dyn -> + let newDyn = DynamicObj() + // might want to keep instance props as dynamic props on copy + for kv in (dyn.GetProperties(true)) do + newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value) + box newDyn | _ -> o - this.GetProperties(false) + this.GetProperties(true) |> Seq.iter (fun kv -> match target.TryGetPropertyHelper kv.Key with | Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value) diff --git a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj index 9dd8ae7..6ccf42b 100644 --- a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj +++ b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj @@ -9,10 +9,25 @@ + + + + + + + + + + + + + + + + - diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs deleted file mode 100644 index c646c43..0000000 --- a/tests/DynamicObject.Tests/DynamicObjs.fs +++ /dev/null @@ -1,721 +0,0 @@ -module DynamicObj.Tests - -open System -open Fable.Pyxpecto -open DynamicObj -open TestUtils - -let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [ - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - let b = a.TryGetPropertyValue "a" - Expect.isNone b "Value should not exist" - - testCase "Correct boxed Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.TryGetPropertyValue "a" - Expect.equal (b) (Some (box 1)) "Value should be 1" - - testCase "Correct unboxed Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.TryGetPropertyValue "a" - Expect.equal (b |> Option.map unbox) (Some 1) "Value should be 1" - - testCase "Correct boxed String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.TryGetPropertyValue "a" - Expect.equal (b) (Some (box "1")) "Value should be '1'" - - testCase "Correct unboxed String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.TryGetPropertyValue "a" - Expect.equal (b |> Option.map unbox) (Some "1") "Value should be '1'" - - testCase "Correct boxed List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.TryGetPropertyValue "a" - Expect.equal (b) (Some (box [1; 2; 3])) "Value should be [1; 2; 3]" - - testCase "Correct unboxed List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.TryGetPropertyValue "a" - Expect.equal (b |> Option.map unbox) (Some [1; 2; 3]) "Value should be [1; 2; 3]" - - testCase "Correct boxed DynamicObj" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - a.SetProperty("a", b) - let c = a.TryGetPropertyValue "a" - Expect.equal (c) (Some (box b)) "Value should be a DynamicObj" - - testCase "Correct unboxed DynamicObj" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - a.SetProperty("a", b) - let c = a.TryGetPropertyValue "a" - Expect.equal (c |> Option.map unbox) (Some b) "Value should be a DynamicObj" - -] - -let tests_GetPropertyValue = testList "GetPropertyValue" [ - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - Expect.throws (fun () -> a.GetPropertyValue("b") |> ignore) "Value should not exist" - - testCase "Correct boxed Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.GetPropertyValue "a" - Expect.equal (b) (box 1) "Value should be 1" - - testCase "Correct unboxed Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.GetPropertyValue "a" - Expect.equal (b |> unbox) (1) "Value should be 1" - - testCase "Correct boxed String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.GetPropertyValue "a" - Expect.equal (b) (box "1") "Value should be '1'" - - testCase "Correct unboxed String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.GetPropertyValue "a" - Expect.equal (b |> unbox) ("1") "Value should be '1'" - - testCase "Correct boxed List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.GetPropertyValue "a" - Expect.equal (b) (box [1; 2; 3]) "Value should be [1; 2; 3]" - - testCase "Correct unboxed List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.GetPropertyValue "a" - Expect.equal (b |> unbox) ([1; 2; 3]) "Value should be [1; 2; 3]" - - testCase "Correct boxed DynamicObj" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - a.SetProperty("a", b) - let c = a.GetPropertyValue "a" - Expect.equal (c) (box b) "Value should be a DynamicObj" - - testCase "Correct unboxed DynamicObj" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - a.SetProperty("a", b) - let c = a.GetPropertyValue "a" - Expect.equal (c |> unbox) (b) "Value should be a DynamicObj" - -] - -#if !FABLE_COMPILER -// instance method TryGetTypedPropertyValue is not Fable-compatible -let tests_TryGetTypedPropertyValue = testList "TryGetTypedPropertyValue" [ - - testCase "typeof" <| fun _ -> - let a = typeof - Expect.equal a.Name "Int32" "Type should be Int32" - - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - let b = a.TryGetTypedPropertyValue "a" - Expect.isNone b "Value should not exist" - - testCase "Correct Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.TryGetTypedPropertyValue "a" - Expect.equal b (Some 1) "Value should be 1" - - testCase "Incorrect Int" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.TryGetTypedPropertyValue "a" - Expect.isNone b "Value should not be an int" - - testCase "Correct String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", "1") - let b = a.TryGetTypedPropertyValue "a" - Expect.equal b (Some "1") "Value should be '1'" - - testCase "Incorrect String" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.TryGetTypedPropertyValue "a" - Expect.isNone b "Value should not be a string" - - testCase "Correct List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.TryGetTypedPropertyValue "a" - Expect.equal b (Some [1; 2; 3]) "Value should be [1; 2; 3]" - - testCase "Incorrect List" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", [1; 2; 3]) - let b = a.TryGetTypedPropertyValue "a" - Expect.isNone b "Value should not be a string list" - - testCase "Correct DynamicObj" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - a.SetProperty("a", b) - let c = a.TryGetTypedPropertyValue "a" - Expect.equal c (Some b) "Value should be a DynamicObj" - - testCase "Incorrect DynamicObj" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = a.TryGetTypedPropertyValue "a" - Expect.isNone b "Value should not be a DynamicObj" -] -#endif - -let tests_TryGetStaticPropertyHelper = testList "TryGetStaticPropertyHelper" [ - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - let b = a.TryGetStaticPropertyHelper("a") - Expect.isNone b "Value should not exist" - - testCase "Properties dictionary is static property" <| fun _ -> - let a = DynamicObj() - let b = Expect.wantSome (a.TryGetStaticPropertyHelper("Properties")) "Value should exist" - Expect.isTrue b.IsStatic "Properties should be static" - Expect.isFalse b.IsDynamic "Properties should not be dynamic" - Expect.isTrue b.IsMutable "Properties should be mutable" - Expect.isFalse b.IsImmutable "Properties should not be immutable" - - testCase "dynamic property not retrieved as static" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - Expect.isNone (a.TryGetStaticPropertyHelper("a")) "dynamic property should not be retrieved via TryGetStaticPropertyInfo" -] - -let tests_TryGetDynamicPropertyHelper = testList "TryGetDynamicPropertyHelper" [ - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - let b = a.TryGetDynamicPropertyHelper("a") - Expect.isNone b "Value should not exist" - - testCase "Existing dynamic property" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = Expect.wantSome (a.TryGetDynamicPropertyHelper("a")) "Value should exist" - Expect.isFalse b.IsStatic "Properties should be static" - Expect.isTrue b.IsDynamic "Properties should not be dynamic" - Expect.isTrue b.IsMutable "Properties should be mutable" - Expect.isFalse b.IsImmutable "Properties should not be immutable" - - testCase "static property not retrieved as dynamic" <| fun _ -> - let a = DynamicObj() - Expect.isNone (a.TryGetDynamicPropertyHelper("Properties")) "static property should not be retrieved via TryGetDynamicPropertyInfo" -] - -let tests_TryGetPropertyHelper = testList "TryGetPropertyHelper" [ - testCase "NonExisting" <| fun _ -> - let a = DynamicObj() - let b = a.TryGetPropertyHelper("a") - Expect.isNone b "Value should not exist" - - testCase "Existing dynamic property" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = Expect.wantSome (a.TryGetPropertyHelper("a")) "Value should exist" - Expect.isFalse b.IsStatic "Properties should be static" - Expect.isTrue b.IsDynamic "Properties should not be dynamic" - Expect.isTrue b.IsMutable "Properties should be mutable" - Expect.isFalse b.IsImmutable "Properties should not be immutable" - - testCase "Existing static property" <| fun _ -> - let a = DynamicObj() - let b = Expect.wantSome (a.TryGetPropertyHelper("Properties")) "Value should exist" - Expect.isTrue b.IsStatic "Properties should be static" - Expect.isFalse b.IsDynamic "Properties should not be dynamic" - Expect.isTrue b.IsMutable "Properties should be mutable" - Expect.isFalse b.IsImmutable "Properties should not be immutable" -] - -let tests_SetProperty = testList "SetProperty" [ - - //TODO: static property accession! - - testCase "Same String" <| fun _ -> - let a = DynamicObj () - a.SetProperty("aaa", 5) - let b = DynamicObj () - b.SetProperty("aaa", 5) - Expect.equal a b "Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" - - testCase "Different Strings" <| fun _ -> - let a = DynamicObj () - a.SetProperty("aaa", 1212) - let b = DynamicObj () - b.SetProperty("aaa", 5) - Expect.notEqual a b "Values should not be equal" - - testCase "String only on one" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - b.SetProperty("aaa", 5) - - Expect.notEqual a b "Values should not be equal" - Expect.notEqual b a "Values should not be equal (Reversed equality)" - - testCase "Same lists different keys" <| fun _ -> - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!1", [1; 2; 3]) - Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal" - - testCase "Different lists" <| fun _ -> - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3; 4; 34]) - Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal" - - testCase "Nested Same List Same String" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3]) - - a.SetProperty("aaa", a') - b.SetProperty("aaa", b') - Expect.equal a' b' "New Values should be equal" - Expect.equal a b "Old Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Old Hash codes should be equal" - Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal" - - testCase "Nested Same List Different Strings" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3]) - - a.SetProperty("aaa", a') - b.SetProperty("aaa1", b') - Expect.equal a' b' "New Values should be equal" - Expect.notEqual a b "Old Values should not be equal" - Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal" - ] - -let tests_RemoveProperty = testList "RemoveProperty" [ - - //TODO: static property removal! - - testCase "Remove" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - a.SetProperty("quack!", "hello") - - a.RemoveProperty "quack!" |> ignore - - Expect.equal a b "Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" - - testCase "Remove Non-Existing" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - a.SetProperty("quack!", "hello") - b.SetProperty("quack!", "hello") - - a.RemoveProperty "quecky!" |> ignore - - Expect.equal a b "Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" - - testCase "Remove only on one" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - a.SetProperty("quack!", "hello") - b.SetProperty("quack!", "hello") - - a.RemoveProperty "quack!" |> ignore - - Expect.notEqual a b "Values should be unequal" - Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal" - - testCase "Nested Remove Non-Existing" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3]) - - a.SetProperty("aaa", a') - a.RemoveProperty "quack!" |> ignore - b.SetProperty("aaa", b') - - Expect.equal a b "Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" - - testCase "Nested Remove only on one" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3]) - - a.SetProperty("aaa", a') - a'.RemoveProperty "quack!" |> ignore - b.SetProperty("aaa", b') - - Expect.notEqual a b "Values should be unequal" - Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal" - - testCase "Nested Remove on both" <| fun _ -> - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetProperty("quack!", [1; 2; 3]) - b'.SetProperty("quack!", [1; 2; 3]) - - a.SetProperty("aaa", a') - a.RemoveProperty "quack!" |> ignore - b.SetProperty("aaa", b') - b.RemoveProperty "quack!" |> ignore - - Expect.equal a b "Values should be equal" - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" - -] - -let tests_GetPropertyHelpers = testList "GetPropertyHelpers" [ - testCase "GetPropertyHelpers" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - a.SetProperty("b", 2) - let properties = a.GetPropertyHelpers(true) - let names = properties |> Seq.map (fun p -> p.Name) - Expect.equal (Seq.toList names) ["a"; "b"] "Should have all properties" -] - -let tests_GetProperties = testList "GetProperties" [ - testCase "GetProperties" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - a.SetProperty("b", 2) - let properties = a.GetProperties(true) |> List.ofSeq - let expected = [ - System.Collections.Generic.KeyValuePair("a", box 1) - System.Collections.Generic.KeyValuePair("b", box 2) - ] - Expect.equal properties expected "Should have all properties" -] - -let tests_ShallowCopyDynamicPropertiesTo = testList "ShallowCopyDynamicPropertiesTo" [ - testCase "ExistingObject" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - a.SetProperty("b", 2) - let b = DynamicObj() - b.SetProperty("c", 3) - a.ShallowCopyDynamicPropertiesTo(b) - Expect.equal (b.GetPropertyValue("a")) 1 "Value a should be copied" - Expect.equal (b.GetPropertyValue("b")) 2 "Value b should be copied" - Expect.equal (b.GetPropertyValue("c")) 3 "Value c should be unaffected" - - testCase "Overwrite" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - let b = DynamicObj() - b.SetProperty("a", 3) - Expect.notEqual a b "Values should not be equal before copying" - a.ShallowCopyDynamicPropertiesTo(b, true) - Expect.equal a b "Values should be equal" - - testCase "copies are only references" <| fun _ -> - let a = DynamicObj() - let inner = DynamicObj() - inner.SetProperty("inner", 1) - a.SetProperty("nested", inner) - let b = DynamicObj() - a.ShallowCopyDynamicPropertiesTo(b) - Expect.equal a b "Value should be copied" - inner.SetProperty("another", 2) - Expect.equal a b "copied value was not mutated via reference" -] - -let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" [ - testCase "NewObject" <| fun _ -> - let a = DynamicObj() - a.SetProperty("a", 1) - a.SetProperty("b", 2) - let b = a.ShallowCopyDynamicProperties() - Expect.equal a b "Values should be equal" - - testCase "copies are only references" <| fun _ -> - let a = DynamicObj() - let inner = DynamicObj() - inner.SetProperty("inner", 1) - a.SetProperty("nested", inner) - let b = a.ShallowCopyDynamicProperties() - Expect.equal a b "Value should be copied" - inner.SetProperty("another", 2) - Expect.equal a b "copied value was not mutated via reference" -] - -type DerivedClass(stat: string, dyn: string) as this = - inherit DynamicObj() - do - this.SetProperty("dyn", dyn) - member this.Stat = stat - -type DerivedClassCloneable(stat: string, dyn: string) as this = - inherit DynamicObj() - do - this.SetProperty("dyn", dyn) - member this.Stat = stat - interface ICloneable with - member this.Clone() = - let dyn = this.GetPropertyValue("dyn") |> unbox - DerivedClassCloneable(stat, dyn) - -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 "DynamicObj list" <| fun _ -> - () - 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 "DynamicObj list" <| fun _ -> - () - 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() - a.SetProperty("b", 2) - Expect.isTrue (a.Equals(a)) "Values should be equal" - - testCase "Different Equal Objects" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", 2) - let a2 = DynamicObj() - a2.SetProperty("b", 2) - Expect.isTrue (a.Equals(a2)) "Values should be equal" - - testCase "Different Unequal Objects" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", 2) - let a2 = DynamicObj() - a2.SetProperty("b", 3) - Expect.isFalse (a.Equals(a2)) "Values should not be equal" - - testCase "nested DynamicObjs" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - b.SetProperty("c", 2) - a.SetProperty("b", b) - let a2 = DynamicObj() - let b2 = DynamicObj() - b2.SetProperty("c", 2) - a2.SetProperty("b", b2) - Expect.isTrue (a.Equals(a2)) "Values should be equal" - -] - -let tests_GetHashCode = testList "GetHashCode" [ - testCase "Same Object" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", 2) - Expect.equal (a.GetHashCode()) (a.GetHashCode()) "Values should be equal" - - testCase "Different Equal Objects" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", 2) - let a2 = DynamicObj() - a2.SetProperty("b", 2) - Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal" - - testCase "Different Unequal Objects" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", 2) - let a2 = DynamicObj() - a.SetProperty("b", 3) - Expect.notEqual (a.GetHashCode()) (a2.GetHashCode()) "Values should not be equal" - - testCase "nested DynamicObjs" <| fun _ -> - let a = DynamicObj() - let b = DynamicObj() - b.SetProperty("c", 2) - a.SetProperty("b", b) - let a2 = DynamicObj() - let b2 = DynamicObj() - b2.SetProperty("c", 2) - a2.SetProperty("b", b2) - Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal" - - testCase "null Value same key" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", null) - let b = DynamicObj() - b.SetProperty("b", null) - Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Values should be equal" - - testCase "null Value different key" <| fun _ -> - let a = DynamicObj() - a.SetProperty("b", null) - let b = DynamicObj() - a.SetProperty("c", null) - Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Values should not be equal" - -] - -let main = testList "DynamicObj (Class)" [ - tests_TryGetPropertyValue - tests_GetPropertyValue - - #if !FABLE_COMPILER - // instance method TryGetTypedValue is not Fable-compatible - tests_TryGetTypedPropertyValue - #endif - - tests_TryGetStaticPropertyHelper - tests_TryGetDynamicPropertyHelper - tests_TryGetPropertyHelper - tests_SetProperty - tests_RemoveProperty - tests_GetPropertyHelpers - tests_GetProperties - tests_ShallowCopyDynamicPropertiesTo - tests_ShallowCopyDynamicProperties - tests_DeepCopyDynamicProperties - tests_Equals - tests_GetHashCode -] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs new file mode 100644 index 0000000..ba79f6a --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs @@ -0,0 +1,233 @@ +module DynamicObj.Tests.DeepCopyDynamicProperties + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ + + 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 = constructDeepCopiedClone 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 = constructDeepCopiedClone ["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 = constructDeepCopiedClone ["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 = constructDeepCopiedClone ["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.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, clone = constructDeepCopiedClone ["list", box l] + item1.SetProperty("item", -1) + item2.SetProperty("item", -1) + item3.SetProperty("item", -1) + let originalProp = original |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) + let clonedProp = clone |> DynObj.getNestedPropAs ["list"] |> List.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.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, clone = constructDeepCopiedClone ["resizeArr", box r] + item1.SetProperty("item", -1) + item2.SetProperty("item", -1) + item3.SetProperty("item", -1) + let originalProp = original |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray + let clonedProp = clone |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray + Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original" + 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, clone = constructDeepCopiedClone ["item", box item] + item.stat <- "mutated" + let originalProp = original |> DynObj.getNestedPropAs["item"] + let clonedProp = clone |> DynObj.getNestedPropAs ["item"] + Expect.equal original clone "Original and clone should be equal after mutating mutable field on original" + 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" + ] + ] + testList "Derived class" [ + testList "Cloneable dynamic properties" [ + testCase "primitives" <| fun _ -> + () + testCase "DynamicObj" <| fun _ -> + () + testCase "DynamicObj array" <| fun _ -> + () + testCase "DynamicObj list" <| fun _ -> + () + testCase "DynamicObj ResizeArray" <| fun _ -> + () + ] + testList "Un-Cloneable dynamic properties" [ + testCase "Class with mutable fields is reference equal" <| fun _ -> + () + ] + testList "static properties" [ + testCase "Class with mutable fields is reference equal" <| fun _ -> + () + ] + ] + testList "Derived class implementing ICloneable" [ + 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 = DerivedClass(stat = "stat", dyn = "dyn") + bulkMutate originalProps original + + let clone = original.DeepCopyDynamicProperties() + 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.sequenceEqual + ( + original.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value) + |> Seq.sortBy fst + ) + ( + Seq.append + mutatedProps + [("dyn", "dyn")] + |> Seq.sortBy fst + ) + "Original should have mutated properties" + Expect.sequenceEqual + ( + clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value) + |> Seq.sortBy fst + ) + ( + Seq.append + originalProps + [("dyn", "dyn"); ("stat", "stat")] // copy should have static prop as dynamic prop + |> Seq.sortBy fst + ) + "Clone should have original and static properties" + Expect.isTrue (original.GetType() = typeof) "Original is of type DerivedClass" + Expect.isTrue (clone.GetType() = typeof) "Clone is of type DynamicObj" + testCase "DynamicObj" <| fun _ -> + () + testCase "DynamicObj array" <| fun _ -> + () + testCase "DynamicObj list" <| fun _ -> + () + testCase "DynamicObj ResizeArray" <| fun _ -> + () + ] + testList "Un-Cloneable dynamic properties" [ + testCase "Class with mutable fields is reference equal" <| fun _ -> + () + ] + ] +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/Equals.fs b/tests/DynamicObject.Tests/DynamicObjs/Equals.fs new file mode 100644 index 0000000..182a94a --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/Equals.fs @@ -0,0 +1,38 @@ +module DynamicObj.Tests.Equals + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_Equals = testList "Equals" [ + testCase "Same Object" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + Expect.isTrue (a.Equals(a)) "Values should be equal" + + testCase "Different Equal Objects" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + let a2 = DynamicObj() + a2.SetProperty("b", 2) + Expect.isTrue (a.Equals(a2)) "Values should be equal" + + testCase "Different Unequal Objects" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + let a2 = DynamicObj() + a2.SetProperty("b", 3) + Expect.isFalse (a.Equals(a2)) "Values should not be equal" + + testCase "nested DynamicObjs" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + b.SetProperty("c", 2) + a.SetProperty("b", b) + let a2 = DynamicObj() + let b2 = DynamicObj() + b2.SetProperty("c", 2) + a2.SetProperty("b", b2) + Expect.isTrue (a.Equals(a2)) "Values should be equal" + +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs b/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs new file mode 100644 index 0000000..5ded1a2 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs @@ -0,0 +1,53 @@ +module DynamicObj.Tests.GetHashCode + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + + +let tests_GetHashCode = testList "GetHashCode" [ + testCase "Same Object" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + Expect.equal (a.GetHashCode()) (a.GetHashCode()) "Values should be equal" + + testCase "Different Equal Objects" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + let a2 = DynamicObj() + a2.SetProperty("b", 2) + Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal" + + testCase "Different Unequal Objects" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", 2) + let a2 = DynamicObj() + a.SetProperty("b", 3) + Expect.notEqual (a.GetHashCode()) (a2.GetHashCode()) "Values should not be equal" + + testCase "nested DynamicObjs" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + b.SetProperty("c", 2) + a.SetProperty("b", b) + let a2 = DynamicObj() + let b2 = DynamicObj() + b2.SetProperty("c", 2) + a2.SetProperty("b", b2) + Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal" + + testCase "null Value same key" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", null) + let b = DynamicObj() + b.SetProperty("b", null) + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Values should be equal" + + testCase "null Value different key" <| fun _ -> + let a = DynamicObj() + a.SetProperty("b", null) + let b = DynamicObj() + a.SetProperty("c", null) + Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Values should not be equal" + +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs new file mode 100644 index 0000000..9aa6108 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs @@ -0,0 +1,28 @@ +module DynamicObj.Tests.GetProperties + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_GetProperties = testList "GetProperties" [ + testCase "GetProperties" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + a.SetProperty("b", 2) + let properties = a.GetProperties(true) |> List.ofSeq + let expected = [ + System.Collections.Generic.KeyValuePair("a", box 1) + System.Collections.Generic.KeyValuePair("b", box 2) + ] + Expect.sequenceEqual properties expected "Should have all properties" + testCase "returns static instance members 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 = + [ + System.Collections.Generic.KeyValuePair("dyn", box "dyn") + System.Collections.Generic.KeyValuePair("stat", box "stat") + ] + |> Seq.sortBy (fun kv -> kv.Key) + Expect.sequenceEqual properties expected "Should have all properties" +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs new file mode 100644 index 0000000..4dca26d --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs @@ -0,0 +1,15 @@ +module DynamicObj.Tests.GetPropertyHelpers + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_GetPropertyHelpers = testList "GetPropertyHelpers" [ + testCase "GetPropertyHelpers" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + a.SetProperty("b", 2) + let properties = a.GetPropertyHelpers(true) + let names = properties |> Seq.map (fun p -> p.Name) + Expect.equal (Seq.toList names) ["a"; "b"] "Should have all properties" +] diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs new file mode 100644 index 0000000..abcf60a --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs @@ -0,0 +1,62 @@ +module DynamicObj.Tests.GetPropertyValue + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_GetPropertyValue = testList "GetPropertyValue" [ + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + Expect.throws (fun () -> a.GetPropertyValue("b") |> ignore) "Value should not exist" + + testCase "Correct boxed Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.GetPropertyValue "a" + Expect.equal (b) (box 1) "Value should be 1" + + testCase "Correct unboxed Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.GetPropertyValue "a" + Expect.equal (b |> unbox) (1) "Value should be 1" + + testCase "Correct boxed String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.GetPropertyValue "a" + Expect.equal (b) (box "1") "Value should be '1'" + + testCase "Correct unboxed String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.GetPropertyValue "a" + Expect.equal (b |> unbox) ("1") "Value should be '1'" + + testCase "Correct boxed List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.GetPropertyValue "a" + Expect.equal (b) (box [1; 2; 3]) "Value should be [1; 2; 3]" + + testCase "Correct unboxed List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.GetPropertyValue "a" + Expect.equal (b |> unbox) ([1; 2; 3]) "Value should be [1; 2; 3]" + + testCase "Correct boxed DynamicObj" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + a.SetProperty("a", b) + let c = a.GetPropertyValue "a" + Expect.equal (c) (box b) "Value should be a DynamicObj" + + testCase "Correct unboxed DynamicObj" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + a.SetProperty("a", b) + let c = a.GetPropertyValue "a" + Expect.equal (c |> unbox) (b) "Value should be a DynamicObj" + +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/Main.fs b/tests/DynamicObject.Tests/DynamicObjs/Main.fs new file mode 100644 index 0000000..4b71c08 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/Main.fs @@ -0,0 +1,31 @@ +module DynamicObjs.Tests + +open Fable.Pyxpecto +open DynamicObj.Tests + +let main = testList "DynamicObj (Class)" [ + + GetHashCode.tests_GetHashCode + Equals.tests_Equals + + SetProperty.tests_SetProperty + RemoveProperty.tests_RemoveProperty + + TryGetPropertyValue.tests_TryGetPropertyValue + GetPropertyValue.tests_GetPropertyValue + + #if !FABLE_COMPILER + // instance method TryGetTypedValue is not Fable-compatible + TryGetTypedPropertyValue.tests_TryGetTypedPropertyValue + #endif + + TryGetStaticPropertyHelper.tests_TryGetStaticPropertyHelper + TryGetDynamicPropertyHelper.tests_TryGetDynamicPropertyHelper + TryGetPropertyHelper.tests_TryGetPropertyHelper + GetPropertyHelpers.tests_GetPropertyHelpers + GetProperties.tests_GetProperties + + ShallowCopyDynamicPropertiesTo.tests_ShallowCopyDynamicPropertiesTo + ShallowCopyDynamicProperties.tests_ShallowCopyDynamicProperties + DeepCopyDynamicProperties.tests_DeepCopyDynamicProperties +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs b/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs new file mode 100644 index 0000000..5cfa82b --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs @@ -0,0 +1,95 @@ +module DynamicObj.Tests.RemoveProperty + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_RemoveProperty = testList "RemoveProperty" [ + + //TODO: static property removal! + + testCase "Remove" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + a.SetProperty("quack!", "hello") + + a.RemoveProperty "quack!" |> ignore + + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + + testCase "Remove Non-Existing" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + a.SetProperty("quack!", "hello") + b.SetProperty("quack!", "hello") + + a.RemoveProperty "quecky!" |> ignore + + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + + testCase "Remove only on one" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + a.SetProperty("quack!", "hello") + b.SetProperty("quack!", "hello") + + a.RemoveProperty "quack!" |> ignore + + Expect.notEqual a b "Values should be unequal" + Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal" + + testCase "Nested Remove Non-Existing" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3]) + + a.SetProperty("aaa", a') + a.RemoveProperty "quack!" |> ignore + b.SetProperty("aaa", b') + + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + + testCase "Nested Remove only on one" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3]) + + a.SetProperty("aaa", a') + a'.RemoveProperty "quack!" |> ignore + b.SetProperty("aaa", b') + + Expect.notEqual a b "Values should be unequal" + Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal" + + testCase "Nested Remove on both" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3]) + + a.SetProperty("aaa", a') + a.RemoveProperty "quack!" |> ignore + b.SetProperty("aaa", b') + b.RemoveProperty "quack!" |> ignore + + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + +] diff --git a/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs b/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs new file mode 100644 index 0000000..fb41979 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs @@ -0,0 +1,78 @@ +module DynamicObj.Tests.SetProperty + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_SetProperty = testList "SetProperty" [ + + //TODO: static property accession! + + testCase "Same String" <| fun _ -> + let a = DynamicObj () + a.SetProperty("aaa", 5) + let b = DynamicObj () + b.SetProperty("aaa", 5) + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + + testCase "Different Strings" <| fun _ -> + let a = DynamicObj () + a.SetProperty("aaa", 1212) + let b = DynamicObj () + b.SetProperty("aaa", 5) + Expect.notEqual a b "Values should not be equal" + + testCase "String only on one" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + b.SetProperty("aaa", 5) + + Expect.notEqual a b "Values should not be equal" + Expect.notEqual b a "Values should not be equal (Reversed equality)" + + testCase "Same lists different keys" <| fun _ -> + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!1", [1; 2; 3]) + Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal" + + testCase "Different lists" <| fun _ -> + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3; 4; 34]) + Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal" + + testCase "Nested Same List Same String" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3]) + + a.SetProperty("aaa", a') + b.SetProperty("aaa", b') + Expect.equal a' b' "New Values should be equal" + Expect.equal a b "Old Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Old Hash codes should be equal" + Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal" + + testCase "Nested Same List Different Strings" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + let a' = DynamicObj () + let b' = DynamicObj () + a'.SetProperty("quack!", [1; 2; 3]) + b'.SetProperty("quack!", [1; 2; 3]) + + a.SetProperty("aaa", a') + b.SetProperty("aaa1", b') + Expect.equal a' b' "New Values should be equal" + Expect.notEqual a b "Old Values should not be equal" + Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal" + ] diff --git a/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs new file mode 100644 index 0000000..59464f3 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs @@ -0,0 +1,24 @@ +module DynamicObj.Tests.ShallowCopyDynamicProperties + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" [ + testCase "NewObject" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + a.SetProperty("b", 2) + let b = a.ShallowCopyDynamicProperties() + Expect.equal a b "Values should be equal" + + testCase "copies are only references" <| fun _ -> + let a = DynamicObj() + let inner = DynamicObj() + inner.SetProperty("inner", 1) + a.SetProperty("nested", inner) + let b = a.ShallowCopyDynamicProperties() + Expect.equal a b "Value should be copied" + inner.SetProperty("another", 2) + Expect.equal a b "copied value was not mutated via reference" +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs new file mode 100644 index 0000000..5df8dc7 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs @@ -0,0 +1,38 @@ +module DynamicObj.Tests.ShallowCopyDynamicPropertiesTo + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_ShallowCopyDynamicPropertiesTo = testList "ShallowCopyDynamicPropertiesTo" [ + testCase "ExistingObject" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + a.SetProperty("b", 2) + let b = DynamicObj() + b.SetProperty("c", 3) + a.ShallowCopyDynamicPropertiesTo(b) + Expect.equal (b.GetPropertyValue("a")) 1 "Value a should be copied" + Expect.equal (b.GetPropertyValue("b")) 2 "Value b should be copied" + Expect.equal (b.GetPropertyValue("c")) 3 "Value c should be unaffected" + + testCase "Overwrite" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = DynamicObj() + b.SetProperty("a", 3) + Expect.notEqual a b "Values should not be equal before copying" + a.ShallowCopyDynamicPropertiesTo(b, true) + Expect.equal a b "Values should be equal" + + testCase "copies are only references" <| fun _ -> + let a = DynamicObj() + let inner = DynamicObj() + inner.SetProperty("inner", 1) + a.SetProperty("nested", inner) + let b = DynamicObj() + a.ShallowCopyDynamicPropertiesTo(b) + Expect.equal a b "Value should be copied" + inner.SetProperty("another", 2) + Expect.equal a b "copied value was not mutated via reference" +] diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs new file mode 100644 index 0000000..77b8a2b --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs @@ -0,0 +1,25 @@ +module DynamicObj.Tests.TryGetDynamicPropertyHelper + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_TryGetDynamicPropertyHelper = testList "TryGetDynamicPropertyHelper" [ + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + let b = a.TryGetDynamicPropertyHelper("a") + Expect.isNone b "Value should not exist" + + testCase "Existing dynamic property" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = Expect.wantSome (a.TryGetDynamicPropertyHelper("a")) "Value should exist" + Expect.isFalse b.IsStatic "Properties should be static" + Expect.isTrue b.IsDynamic "Properties should not be dynamic" + Expect.isTrue b.IsMutable "Properties should be mutable" + Expect.isFalse b.IsImmutable "Properties should not be immutable" + + testCase "static property not retrieved as dynamic" <| fun _ -> + let a = DynamicObj() + Expect.isNone (a.TryGetDynamicPropertyHelper("Properties")) "static property should not be retrieved via TryGetDynamicPropertyInfo" +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs new file mode 100644 index 0000000..89091c0 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs @@ -0,0 +1,29 @@ +module DynamicObj.Tests.TryGetPropertyHelper + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_TryGetPropertyHelper = testList "TryGetPropertyHelper" [ + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + let b = a.TryGetPropertyHelper("a") + Expect.isNone b "Value should not exist" + + testCase "Existing dynamic property" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = Expect.wantSome (a.TryGetPropertyHelper("a")) "Value should exist" + Expect.isFalse b.IsStatic "Properties should be static" + Expect.isTrue b.IsDynamic "Properties should not be dynamic" + Expect.isTrue b.IsMutable "Properties should be mutable" + Expect.isFalse b.IsImmutable "Properties should not be immutable" + + testCase "Existing static property" <| fun _ -> + let a = DynamicObj() + let b = Expect.wantSome (a.TryGetPropertyHelper("Properties")) "Value should exist" + Expect.isTrue b.IsStatic "Properties should be static" + Expect.isFalse b.IsDynamic "Properties should not be dynamic" + Expect.isTrue b.IsMutable "Properties should be mutable" + Expect.isFalse b.IsImmutable "Properties should not be immutable" +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs new file mode 100644 index 0000000..7c86a66 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs @@ -0,0 +1,63 @@ +module DynamicObj.Tests.TryGetPropertyValue + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [ + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + let b = a.TryGetPropertyValue "a" + Expect.isNone b "Value should not exist" + + testCase "Correct boxed Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.TryGetPropertyValue "a" + Expect.equal (b) (Some (box 1)) "Value should be 1" + + testCase "Correct unboxed Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.TryGetPropertyValue "a" + Expect.equal (b |> Option.map unbox) (Some 1) "Value should be 1" + + testCase "Correct boxed String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.TryGetPropertyValue "a" + Expect.equal (b) (Some (box "1")) "Value should be '1'" + + testCase "Correct unboxed String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.TryGetPropertyValue "a" + Expect.equal (b |> Option.map unbox) (Some "1") "Value should be '1'" + + testCase "Correct boxed List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.TryGetPropertyValue "a" + Expect.equal (b) (Some (box [1; 2; 3])) "Value should be [1; 2; 3]" + + testCase "Correct unboxed List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.TryGetPropertyValue "a" + Expect.equal (b |> Option.map unbox) (Some [1; 2; 3]) "Value should be [1; 2; 3]" + + testCase "Correct boxed DynamicObj" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + a.SetProperty("a", b) + let c = a.TryGetPropertyValue "a" + Expect.equal (c) (Some (box b)) "Value should be a DynamicObj" + + testCase "Correct unboxed DynamicObj" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + a.SetProperty("a", b) + let c = a.TryGetPropertyValue "a" + Expect.equal (c |> Option.map unbox) (Some b) "Value should be a DynamicObj" + +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs new file mode 100644 index 0000000..2fcfc11 --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs @@ -0,0 +1,25 @@ +module DynamicObj.Tests.TryGetStaticPropertyHelper + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +let tests_TryGetStaticPropertyHelper = testList "TryGetStaticPropertyHelper" [ + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + let b = a.TryGetStaticPropertyHelper("a") + Expect.isNone b "Value should not exist" + + testCase "Properties dictionary is static property" <| fun _ -> + let a = DynamicObj() + let b = Expect.wantSome (a.TryGetStaticPropertyHelper("Properties")) "Value should exist" + Expect.isTrue b.IsStatic "Properties should be static" + Expect.isFalse b.IsDynamic "Properties should not be dynamic" + Expect.isTrue b.IsMutable "Properties should be mutable" + Expect.isFalse b.IsImmutable "Properties should not be immutable" + + testCase "dynamic property not retrieved as static" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + Expect.isNone (a.TryGetStaticPropertyHelper("a")) "dynamic property should not be retrieved via TryGetStaticPropertyInfo" +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs new file mode 100644 index 0000000..38d835f --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs @@ -0,0 +1,69 @@ +module DynamicObj.Tests.TryGetTypedPropertyValue + +open Fable.Pyxpecto +open DynamicObj +open TestUtils + +#if !FABLE_COMPILER +// instance method TryGetTypedPropertyValue is not Fable-compatible +let tests_TryGetTypedPropertyValue = testList "TryGetTypedPropertyValue" [ + + testCase "typeof" <| fun _ -> + let a = typeof + Expect.equal a.Name "Int32" "Type should be Int32" + + testCase "NonExisting" <| fun _ -> + let a = DynamicObj() + let b = a.TryGetTypedPropertyValue "a" + Expect.isNone b "Value should not exist" + + testCase "Correct Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.TryGetTypedPropertyValue "a" + Expect.equal b (Some 1) "Value should be 1" + + testCase "Incorrect Int" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.TryGetTypedPropertyValue "a" + Expect.isNone b "Value should not be an int" + + testCase "Correct String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", "1") + let b = a.TryGetTypedPropertyValue "a" + Expect.equal b (Some "1") "Value should be '1'" + + testCase "Incorrect String" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.TryGetTypedPropertyValue "a" + Expect.isNone b "Value should not be a string" + + testCase "Correct List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.TryGetTypedPropertyValue "a" + Expect.equal b (Some [1; 2; 3]) "Value should be [1; 2; 3]" + + testCase "Incorrect List" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", [1; 2; 3]) + let b = a.TryGetTypedPropertyValue "a" + Expect.isNone b "Value should not be a string list" + + testCase "Correct DynamicObj" <| fun _ -> + let a = DynamicObj() + let b = DynamicObj() + a.SetProperty("a", b) + let c = a.TryGetTypedPropertyValue "a" + Expect.equal c (Some b) "Value should be a DynamicObj" + + testCase "Incorrect DynamicObj" <| fun _ -> + let a = DynamicObj() + a.SetProperty("a", 1) + let b = a.TryGetTypedPropertyValue "a" + Expect.isNone b "Value should not be a DynamicObj" +] +#endif \ No newline at end of file diff --git a/tests/DynamicObject.Tests/Main.fs b/tests/DynamicObject.Tests/Main.fs index 8b79a73..a067e8c 100644 --- a/tests/DynamicObject.Tests/Main.fs +++ b/tests/DynamicObject.Tests/Main.fs @@ -4,7 +4,7 @@ open Fable.Pyxpecto let all = testSequenced <| testList "DynamicObj" [ ReflectionUtils.Tests.main - DynamicObj.Tests.main + DynamicObjs.Tests.main DynObj.Tests.main Inheritance.Tests.main Interface.Tests.main diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs index caef957..0c08606 100644 --- a/tests/DynamicObject.Tests/TestUtils.fs +++ b/tests/DynamicObject.Tests/TestUtils.fs @@ -1,6 +1,42 @@ module TestUtils +open System open DynamicObj +open Fable.Core + +[] +type MutableClass(stat:string) = + let mutable s = stat + member this.stat with get() = s and set v = s <- v + +[] +type DerivedClass(stat: string, dyn: string) as this = + inherit DynamicObj() + do + this.SetProperty("dyn", dyn) + member this.stat = stat + +[] +type DerivedClassCloneable(stat: string, dyn: string) as this = + inherit DynamicObj() + do + this.SetProperty("dyn", dyn) + member this.stat = stat + interface ICloneable with + member this.Clone() = + let dyn = this.GetPropertyValue("dyn") |> unbox + DerivedClassCloneable(stat, dyn) + +let constructDeepCopiedClone (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)) + let firstDiff s1 s2 = let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None)) @@ -31,4 +67,8 @@ module Expect = 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 + message i a + + let referenceEqual actual expected message = + if not (LanguagePrimitives.PhysicalEquality actual expected) then + failwith message \ No newline at end of file From b518f9a338f1f07630ba75c3e5c0a1817606087c Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Fri, 13 Dec 2024 15:34:40 +0100 Subject: [PATCH 5/6] Add deep copy tests for derived classes --- src/DynamicObj/DynamicObj.fs | 33 ++++++- .../DynamicObjs/DeepCopyDynamicProperties.fs | 89 +++++++++++++++++-- .../DynamicObjs/GetProperties.fs | 2 +- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 16bd912..5c13094 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -245,9 +245,9 @@ type DynamicObj() = ) /// - /// 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 @@ -331,6 +331,35 @@ type DynamicObj() = this.ShallowCopyDynamicPropertiesTo(target) target + /// + /// 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<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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. + /// + /// The target object to copy dynamic members to + /// Whether existing properties on the target object will be overwritten member this.DeepCopyDynamicProperties() = let target = DynamicObj() this.DeepCopyDynamicPropertiesTo(target) diff --git a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs index ba79f6a..3c50956 100644 --- a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs +++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs @@ -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 _ -> () @@ -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["mutable"] + let clonedProp = clone |> DynObj.getNestedPropAs ["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 = [ @@ -216,18 +232,77 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ "Clone should have original and static properties" Expect.isTrue (original.GetType() = typeof) "Original is of type DerivedClass" Expect.isTrue (clone.GetType() = typeof) "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 ["inner";"inner int"]) 1 "Original should have mutated properties" + Expect.equal (clone |> DynObj.getNestedPropAs ["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 ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) + let clonedProp = clone |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["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 ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) + let clonedProp = clone |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["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> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray + let clonedProp = clone |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["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["item"] + let clonedProp = clone |> DynObj.getNestedPropAs ["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" ] ] ] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs index 9aa6108..1cf3845 100644 --- a/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs +++ b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs @@ -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 = From ddfd632e6c22857b6f628ef5898f87fd93a20553 Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Tue, 17 Dec 2024 10:43:00 +0100 Subject: [PATCH 6/6] Add conditional transpilation for matching against System.ICloneable, add tests --- build/BasicTasks.fs | 2 + build/TestTasks.fs | 4 +- src/DynamicObj/DynamicObj.fs | 102 ++++++++++-------- src/DynamicObj/FableJS.fs | 9 ++ src/DynamicObj/FablePy.fs | 8 ++ .../DynamicObjs/DeepCopyDynamicProperties.fs | 91 ++++++++++------ tests/DynamicObject.Tests/TestUtils.fs | 6 +- 7 files changed, 136 insertions(+), 86 deletions(-) diff --git a/build/BasicTasks.fs b/build/BasicTasks.fs index 93bc5b4..3b5ec8e 100644 --- a/build/BasicTasks.fs +++ b/build/BasicTasks.fs @@ -152,6 +152,8 @@ let clean = BuildTask.create "Clean" [] { ++ "src/**/obj" ++ "tests/**/bin" ++ "tests/**/obj" + ++ "tests/**/js" + ++ "tests/**/py" ++ "dist" ++ ProjectInfo.netPkgDir |> Shell.cleanDirs diff --git a/build/TestTasks.fs b/build/TestTasks.fs index b5d712f..d95ce7e 100644 --- a/build/TestTasks.fs +++ b/build/TestTasks.fs @@ -22,7 +22,7 @@ module RunTests = let runTestsJs = BuildTask.create "runTestsJS" [clean; build] { for path in ProjectInfo.fableTestProjects do // transpile js files from fsharp code - run dotnet $"fable {path} -o {path}/js" "" + run dotnet $"fable {path} -o {path}/js --noCache" "" // run mocha in target path to execute tests // "--timeout 20000" is used, because json schema validation takes a bit of time. run node $"{path}/js/Main.js" "" @@ -40,7 +40,7 @@ module RunTests = let runTestsPy = BuildTask.create "runTestsPy" [clean; build] { for path in ProjectInfo.fableTestProjects do //transpile py files from fsharp code - run dotnet $"fable {path} -o {path}/py --lang python" "" + run dotnet $"fable {path} -o {path}/py --lang python --noCache" "" // run pyxpecto in target path to execute tests in python run python $"{path}/py/main.py" "" } diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 5c13094..43bd318 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -245,36 +245,19 @@ type DynamicObj() = ) /// - /// Attempts to deep copy the properties of the DynamicObj onto the target. - /// - /// 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<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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: + /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties). /// - /// 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. + /// Note that this function does not attempt to do any deep copying. + /// The dynamic properties of the source will be copied as references to the target. + /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target. /// - /// 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 + member this.ShallowCopyDynamicProperties() = + let target = DynamicObj() + this.ShallowCopyDynamicPropertiesTo(target, true) + target + + // internal helper function to deep copy a boxed object (if possible) + static member internal tryDeepCopyObj (o:obj) = let rec tryDeepCopyObj (o:obj) = match o with @@ -298,9 +281,14 @@ type DynamicObj() = box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj] | :? ResizeArray as dyns -> box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj])) - - #if !FABLE_COMPILER_PYTHON + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + | o when FableJS.Interfaces.implementsICloneable o -> FableJS.Interfaces.cloneICloneable o + #endif + #if FABLE_COMPILER_PYTHON // https://github.com/fable-compiler/Fable/issues/3972 + | o when FablePy.Interfaces.implementsICloneable o -> FablePy.Interfaces.cloneICloneable o + #endif + #if !FABLE_COMPILER | :? System.ICloneable as clonable -> clonable.Clone() #endif @@ -312,24 +300,47 @@ type DynamicObj() = box newDyn | _ -> o + tryDeepCopyObj o + + /// + /// Attempts to deep copy the properties of the DynamicObj onto the target. + /// + /// 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<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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. + /// + /// 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 + this.GetProperties(true) |> Seq.iter (fun kv -> match target.TryGetPropertyHelper kv.Key with - | Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value) + | Some pi when overWrite -> pi.SetValue target (DynamicObj.tryDeepCopyObj kv.Value) | Some _ -> () - | None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value) + | None -> target.SetProperty(kv.Key, DynamicObj.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. - /// The dynamic properties of the source will be copied as references to the target. - /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target. - /// - member this.ShallowCopyDynamicProperties() = - let target = DynamicObj() - this.ShallowCopyDynamicPropertiesTo(target) - target /// /// Attempts to perform a deep copy of the DynamicObj. @@ -360,10 +371,7 @@ type DynamicObj() = /// /// The target object to copy dynamic members to /// Whether existing properties on the target object will be overwritten - member this.DeepCopyDynamicProperties() = - let target = DynamicObj() - this.DeepCopyDynamicPropertiesTo(target) - target + member this.DeepCopyDynamicProperties() = DynamicObj.tryDeepCopyObj this #if !FABLE_COMPILER // Some necessary overrides for methods inherited from System.Dynamic.DynamicObject() diff --git a/src/DynamicObj/FableJS.fs b/src/DynamicObj/FableJS.fs index 7d889b7..ff457ff 100644 --- a/src/DynamicObj/FableJS.fs +++ b/src/DynamicObj/FableJS.fs @@ -158,5 +158,14 @@ module FableJS = getPropertyHelpers o |> Array.map (fun h -> h.Name) + module Interfaces = + + [] + let implementsICloneable (o:obj) : bool = + jsNative + + [] + let cloneICloneable (o:obj) : obj = + jsNative #endif \ No newline at end of file diff --git a/src/DynamicObj/FablePy.fs b/src/DynamicObj/FablePy.fs index 4d706b9..1bc575e 100644 --- a/src/DynamicObj/FablePy.fs +++ b/src/DynamicObj/FablePy.fs @@ -211,5 +211,13 @@ module FablePy = getPropertyHelpers o |> Array.map (fun h -> h.Name) + module Interfaces = + + [] + let implementsICloneable (o:obj) : bool = + nativeOnly + [] + let cloneICloneable (o:obj) : obj = + nativeOnly #endif \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs index 3c50956..a0abd9a 100644 --- a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs +++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs @@ -26,7 +26,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ "single", box (single 1.0f) "decimal", box (decimal 1M) ] - let original, clone = constructDeepCopiedClone originalProps + let original, clone = constructDeepCopiedClone originalProps let mutatedProps = [ "int", box 2 "float", box 2.0 @@ -51,7 +51,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ testCase "DynamicObj" <| fun _ -> let inner = DynamicObj() |> DynObj.withProperty "inner int" 2 - let original, clone = constructDeepCopiedClone ["dyn", inner] + let original, clone = constructDeepCopiedClone ["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" @@ -61,7 +61,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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 = constructDeepCopiedClone ["first_level", first_level] + let original, clone = constructDeepCopiedClone ["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" @@ -72,7 +72,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let item2 = DynamicObj() |> DynObj.withProperty "item" 2 let item3 = DynamicObj() |> DynObj.withProperty "item" 3 let arr = [|item1; item2; item3|] - let original, clone = constructDeepCopiedClone ["arr", box arr] + let original, clone = constructDeepCopiedClone ["arr", box arr] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -87,7 +87,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let item2 = DynamicObj() |> DynObj.withProperty "item" 2 let item3 = DynamicObj() |> DynObj.withProperty "item" 3 let l = [item1; item2; item3] - let original, clone = constructDeepCopiedClone ["list", box l] + let original, clone = constructDeepCopiedClone ["list", box l] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -102,7 +102,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let item2 = DynamicObj() |> DynObj.withProperty "item" 2 let item3 = DynamicObj() |> DynObj.withProperty "item" 3 let r = ResizeArray([item1; item2; item3]) - let original, clone = constructDeepCopiedClone ["resizeArr", box r] + let original, clone = constructDeepCopiedClone ["resizeArr", box r] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -115,7 +115,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ testList "Un-Cloneable dynamic properties" [ testCase "Class with mutable fields is reference equal" <| fun _ -> let item = MutableClass("initial") - let original, clone = constructDeepCopiedClone ["item", box item] + let original, clone = constructDeepCopiedClone ["item", box item] item.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["item"] let clonedProp = clone |> DynObj.getNestedPropAs ["item"] @@ -126,39 +126,60 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ ] ] testList "Derived class implementing ICloneable" [ - testList "Cloneable dynamic properties" [ - testCase "primitives" <| fun _ -> - () - testCase "DynamicObj" <| fun _ -> - () - testCase "DynamicObj array" <| fun _ -> - () - testCase "DynamicObj list" <| fun _ -> - () - testCase "DynamicObj ResizeArray" <| fun _ -> - () - ] - testList "Un-Cloneable dynamic properties" [ - testCase "Class with mutable fields is reference equal" <| fun _ -> - () - ] - testList "static properties" [ - testCase "Class with mutable fields is reference equal" <| fun _ -> - () + testList "SpecialCases" [ + testCase "can unbox copy as DerivedClassCloneable" <| fun _ -> + Expect.pass ( + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + () + ) + testCase "copy is of type DerivedClassCloneable" <| fun _ -> + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + Expect.equal (clone.GetType()) typeof "Clone is of type DerivedClassCloneable" + ptestCase "copy has NO instance prop as dynamic prop" <| fun _ -> + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value) + Expect.sequenceEqual clonedProps ["dyn", "dyn"] "Clone should have no dynamic properties" + testCase "copy has static and dynamic props of original" <| fun _ -> + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + Expect.equal clone original "Clone and original should be equal" + Expect.equal (clone.stat) (original.stat) "Clone should have static prop from derived class" + Expect.equal (clone |> DynObj.getNestedPropAs ["dyn"]) (original |> DynObj.getNestedPropAs ["dyn"]) "Clone should have dynamic prop from derived class" + testCase "can use instance method on copied derived class" <| fun _ -> + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + Expect.pass (clone.PrintStat()) + testCase "instance method on copied derived class returns correct value" <| fun _ -> + let original = DerivedClassCloneable(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() |> unbox + Expect.equal (clone.FormatStat()) "stat: stat" "instance method should return correct value" ] ] testList "Derived class" [ testList "SpecialCases" [ + + #if !FABLE_COMPILER + // this test is transpiled as Expect_throws(() => {} and can never fail, so let's just test it in F# for now + testCase "Cannot unbox clone as original type" <| fun _ -> + let original = DerivedClass(stat = "stat", dyn = "dyn") + let clone = original.DeepCopyDynamicProperties() + let unboxMaybe() = clone |> unbox |> ignore + Expect.throws unboxMaybe "Clone cannot be unboxed as DerivedClass" + #endif + testCase "copy has instance prop as dynamic prop" <| fun _ -> let original = DerivedClass(stat = "stat", dyn = "dyn") - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox 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() + let clone = original.DeepCopyDynamicProperties() |> unbox mut.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["mutable"] let clonedProp = clone |> DynObj.getNestedPropAs ["mutable"] @@ -186,7 +207,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let original = DerivedClass(stat = "stat", dyn = "dyn") bulkMutate originalProps original - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox let mutatedProps = [ "int", box 2 "float", box 2.0 @@ -237,7 +258,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let inner = DynamicObj() |> DynObj.withProperty "inner int" 2 let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("inner", inner) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox inner.SetProperty("inner int", 1) Expect.equal (original |> DynObj.getNestedPropAs ["inner";"inner int"]) 1 "Original should have mutated properties" @@ -250,7 +271,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let arr = [|item1; item2; item3|] let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("arr", arr) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -264,9 +285,9 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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") + let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("list", l) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -282,7 +303,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let r = ResizeArray([item1; item2; item3]) let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("resizeArr", r) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -296,7 +317,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let item = MutableClass("initial") let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("item", item) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["item"] let clonedProp = clone |> DynObj.getNestedPropAs ["item"] diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs index 0c08606..4d86ece 100644 --- a/tests/DynamicObject.Tests/TestUtils.fs +++ b/tests/DynamicObject.Tests/TestUtils.fs @@ -22,17 +22,19 @@ type DerivedClassCloneable(stat: string, dyn: string) as this = do this.SetProperty("dyn", dyn) member this.stat = stat + member this.FormatStat() = $"stat: {this.stat}" + member this.PrintStat() = this.FormatStat() |> printfn "%s" interface ICloneable with member this.Clone() = let dyn = this.GetPropertyValue("dyn") |> unbox DerivedClassCloneable(stat, dyn) -let constructDeepCopiedClone (props: seq) = +let constructDeepCopiedClone<'T> (props: seq) = let original = DynamicObj() props |> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue)) let clone = original.DeepCopyDynamicProperties() - original, clone + original, clone |> unbox<'T> let bulkMutate (props: seq) (dyn: #DynamicObj) = props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))