From 52601b24325fa2de3ff51961936b8c990e06370b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Denuzi=C3=A8re?= Date: Wed, 18 Sep 2024 17:07:07 +0200 Subject: [PATCH] feat: #26 check for nullness --- src/Diffract/DiffPrinter.fs | 20 ++++++++--- src/Diffract/Differ.fs | 13 +++++++- src/Diffract/Types.fs | 2 ++ tests/Diffract.Tests/Tests.fs | 62 +++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/Diffract/DiffPrinter.fs b/src/Diffract/DiffPrinter.fs index 0ecacba..41233bc 100644 --- a/src/Diffract/DiffPrinter.fs +++ b/src/Diffract/DiffPrinter.fs @@ -11,15 +11,27 @@ let toStreamImpl (w: TextWriter) param (d: Diff) = let indentLike str = String.replicate (String.length str) " " let displayPath path = if path = "" then param.neutralName else path - let printValue indent path x1 x2 = + let printValue indent path (x1: obj) (x2: obj) = let dpath = if path = "" then "" else path + " " - w.WriteLine($"%s{indent}%s{dpath}%s{param.x1Name} = %A{x1}") - w.WriteLine($"%s{indent}%s{indentLike dpath}%s{param.x2Name} = %A{x2}") - + if isNull x1 then + w.WriteLine($"%s{indent}%s{dpath}%s{param.x1Name} is null") + w.WriteLine($"%s{indent}%s{indentLike dpath}%s{param.x2Name} = %A{x2}") + elif isNull x2 then + w.WriteLine($"%s{indent}%s{dpath}%s{param.x1Name} = %A{x1}") + w.WriteLine($"%s{indent}%s{indentLike dpath}%s{param.x2Name} is null") + else + w.WriteLine($"%s{indent}%s{dpath}%s{param.x1Name} = %A{x1}") + w.WriteLine($"%s{indent}%s{indentLike dpath}%s{param.x2Name} = %A{x2}") + let rec loop (indent: string) (path: string) (d: Diff) = match d with | Diff.Value (x1, x2) -> printValue indent path x1 x2 + | Diff.Nullness (x1, x2) -> + let dpath = if path = "" then "" else path + " " + let dindent = indentLike dpath + w.WriteLine($"""%s{indent}%s{dpath}%s{param.x1Name} is%s{if isNull x1 then "" else " not"} null""") + w.WriteLine($"""%s{indent}%s{dindent}%s{param.x2Name} is%s{if isNull x2 then "" else " not"} null""") | Diff.Record fields when fields.Count = 1 -> loop indent (addPathField path fields.[0].Name) fields.[0].Diff | Diff.Record fields -> diff --git a/src/Diffract/Differ.fs b/src/Diffract/Differ.fs index 0efc327..a0c1f0b 100644 --- a/src/Diffract/Differ.fs +++ b/src/Diffract/Differ.fs @@ -13,6 +13,17 @@ module DifferImpl = type private Cache = Dictionary type private CachedDiffer<'T> = Cache -> IDiffer<'T> + let private checkNull<'T> (differ: IDiffer<'T>) = + if typeof<'T>.IsValueType then + differ + else + { new IDiffer<'T> with + member _.Diff(x1, x2) = + match isNull (box x1), isNull (box x2) with + | false, false -> differ.Diff(x1, x2) + | true, true -> None + | _ -> Some (Nullness(x1, x2)) } + /// Add to the cache a differ for a type that may be recursive (ie have nested fields of its own type). let private addRecursiveToCache (differ: CachedDiffer<'T>) : CachedDiffer<'T> = let ty = typeof<'T> @@ -25,7 +36,7 @@ module DifferImpl = { new IDiffer<'U> with member _.Diff(x1, x2) = (unbox> !r).Diff(x1, x2) } }) - let differ = differ cache + let differ = differ cache |> checkNull r := differ differ | true, differFactory -> diff --git a/src/Diffract/Types.fs b/src/Diffract/Types.fs index dcb0db8..abad8a8 100644 --- a/src/Diffract/Types.fs +++ b/src/Diffract/Types.fs @@ -9,6 +9,8 @@ open TypeShape.Core type Diff = /// The objects are leaf values and are different. | Value of x1: obj * x2: obj + /// One of the objects is null and the other isn't. + | Nullness of x1: obj * x2: obj /// The objects are records or plain objects and some of their fields differ. | Record of fields: IReadOnlyList /// The objects are F# unions with different cases. diff --git a/tests/Diffract.Tests/Tests.fs b/tests/Diffract.Tests/Tests.fs index 5c9da6a..b79b96c 100644 --- a/tests/Diffract.Tests/Tests.fs +++ b/tests/Diffract.Tests/Tests.fs @@ -13,6 +13,12 @@ type U = | U2 of x: int * y: int type Bar = { aaa: Foo; bbb: U } +type Baz = { xx: int; yy: string } + +[] +type Class(x: int) = + member _.X = x + let assertStr (expected: string, actual: string) = Assert.Equal(expected.Replace("\r\n", "\n"), actual) @@ -27,6 +33,62 @@ let ``Exception for non-equal values`` (x: Bar) (y: Bar) = Differ.Assert(x, y)) |> ignore +[] +let ``Both have null leaf`` () = + let actual = Differ.Diff({ xx = 1; yy = null }, { xx = 1; yy = null }) + Assert.Equal(None, actual) + +[] +let ``Expected has null leaf`` () = + let actual = Differ.Diff({ xx = 1; yy = null }, { xx = 1; yy = "a" }) + Assert.Equal(Some(Diff.Record [ { Name = "yy"; Diff = Diff.Value(null, "a") } ]), actual) + + let actual = Differ.ToString({ xx = 1; yy = null }, { xx = 1; yy = "a" }) + assertStr("\ +yy Expect is null + Actual = \"a\" +", actual) + +[] +let ``Actual has null leaf`` () = + let actual = Differ.Diff({ xx = 1; yy = "a" }, { xx = 1; yy = null }) + Assert.Equal(Some(Diff.Record [ { Name = "yy"; Diff = Diff.Value("a", null) } ]), actual) + + let actual = Differ.ToString({ xx = 1; yy = "a" }, { xx = 1; yy = null }) + assertStr("\ +yy Expect = \"a\" + Actual is null +", actual) + +[] +let ``Both are null`` () = + let actual = Differ.Diff((null: Class), null) + Assert.Equal(None, actual) + +[] +let ``Expected is null`` () = + let actualValue = Class(3) + let actual = Differ.Diff(null, actualValue) + Assert.Equal(Some(Diff.Nullness(null, actualValue)), actual) + + let actual = Differ.ToString(null, actualValue) + assertStr("\ +Expect is null +Actual is not null +", actual) + +[] +let ``Actual is null`` () = + let actualValue = Class(3) + let actual = Differ.Diff(actualValue, null) + Assert.Equal(Some(Diff.Nullness(actualValue, null)), actual) + + let actual = Differ.ToString(actualValue, null) + assertStr("\ +Expect is not null +Actual is null +", actual) + [] let ``List diff`` (l1: int list) (l2: int list) = let d = Differ.Diff(l1, l2)