Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#26 Check for nullness #27

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/Diffract/DiffPrinter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
13 changes: 12 additions & 1 deletion src/Diffract/Differ.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ module DifferImpl =
type private Cache = Dictionary<Type, IDifferFactory>
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>
Expand All @@ -25,7 +36,7 @@ module DifferImpl =
{ new IDiffer<'U> with
member _.Diff(x1, x2) =
(unbox<IDiffer<'U>> !r).Diff(x1, x2) } })
let differ = differ cache
let differ = differ cache |> checkNull
r := differ
differ
| true, differFactory ->
Expand Down
2 changes: 2 additions & 0 deletions src/Diffract/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FieldDiff>
/// The objects are F# unions with different cases.
Expand Down
62 changes: 62 additions & 0 deletions tests/Diffract.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ type U =
| U2 of x: int * y: int
type Bar = { aaa: Foo; bbb: U }

type Baz = { xx: int; yy: string }

[<AllowNullLiteral>]
type Class(x: int) =
member _.X = x

let assertStr (expected: string, actual: string) =
Assert.Equal(expected.Replace("\r\n", "\n"), actual)

Expand All @@ -27,6 +33,62 @@ let ``Exception for non-equal values`` (x: Bar) (y: Bar) =
Differ.Assert(x, y))
|> ignore

[<Fact>]
let ``Both have null leaf`` () =
let actual = Differ.Diff({ xx = 1; yy = null }, { xx = 1; yy = null })
Assert.Equal(None, actual)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
let ``Both are null`` () =
let actual = Differ.Diff((null: Class), null)
Assert.Equal(None, actual)

[<Fact>]
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)

[<Fact>]
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)

[<Property>]
let ``List diff`` (l1: int list) (l2: int list) =
let d = Differ.Diff(l1, l2)
Expand Down
Loading