diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 4ccc84aaa49..28a3905257b 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1941,7 +1941,7 @@ defmodule String do end @doc """ - Returns a keyword list that represents an edit graph. + Returns a keyword list that represents an edit script. The algorithm is outlined in the "An O(ND) Difference Algorithm and Its Variations" paper by E. Myers. diff --git a/lib/ex_unit/examples/difference.exs b/lib/ex_unit/examples/difference.exs index 10e4217fa86..5f5e065baf1 100644 --- a/lib/ex_unit/examples/difference.exs +++ b/lib/ex_unit/examples/difference.exs @@ -74,8 +74,8 @@ defmodule Difference do end test "maps; mixed diff" do - map1 = Enum.into(1..40, %{}, &{&1, &1}) |> Map.delete(33) - map2 = Enum.reduce(5..10, map1, &Map.delete(&2, &1)) |> Map.put(33, 33) |> Map.put(23, 32) + map1 = Enum.into(1..15, %{}, &{&1, &1}) |> Map.delete(13) + map2 = Enum.reduce(5..10, map1, &Map.delete(&2, &1)) |> Map.put(13, 13) |> Map.put(12, 32) assert map1 == map2 end diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index ee3b91cc120..a6a7c8aba00 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -3,8 +3,6 @@ defmodule ExUnit.AssertionError do Raised to signal an assertion error. """ - alias ExUnit.Formatter, as: F - @no_value :ex_unit_no_meaningful_value defexception left: @no_value, @@ -19,8 +17,9 @@ defmodule ExUnit.AssertionError do @no_value end - def message(assertion_error) do - "\n\n" <> F.format_assertion_error(assertion_error, :infinity, &formatter/2, "") + def message(exception) do + "\n\n" <> + ExUnit.Formatter.format_assertion_error(exception, :infinity, &formatter/2, "") end defp formatter(_, msg), do: msg diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 7153791eb47..30bd4679f81 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -2,44 +2,50 @@ defmodule ExUnit.Diff do @moduledoc false @doc """ - Formats the difference between `left` and `right`. + Returns an edit script representing the difference between `left` and `right`. Returns `nil` if they are not the same data type, or if the given data type is not supported. """ - def format(left, right, formatter) + def script(left, right) + + def script(term, term) + when is_binary(term) or is_number(term) + when is_map(term) or is_list(term) or is_tuple(term) do + [eq: inspect(term)] + end # Binaries - def format(left, right, formatter) when is_binary(left) and is_binary(right) do + def script(left, right) when is_binary(left) and is_binary(right) do if String.printable?(left) and String.printable?(right) do length1 = String.length(left) length2 = String.length(right) if bag_distance(left, right) / max(length1, length2) <= 0.6 do left = Inspect.BitString.escape(left, ?\") right = Inspect.BitString.escape(right, ?\") - "\"" <> format_string(left, right, formatter) <> "\"" + [{:eq, "\""}, script_string(left, right), {:eq, "\""}] end end end # Structs - def format(%name{} = left, %name{} = right, formatter) do + def script(%name{} = left, %name{} = right) do left = Map.from_struct(left) right = Map.from_struct(right) - format_map(left, right, inspect(name), formatter) + script_map(left, right, inspect(name)) end # Maps - def format(%{} = left, %{} = right, formatter) do + def script(%{} = left, %{} = right) do if match?(%_{}, left) or match?(%_{}, right) do nil else - format_map(left, right, "", formatter) + script_map(left, right, "") end end # Char lists and lists - def format(left, right, formatter) when is_list(left) and is_list(right) do + def script(left, right) when is_list(left) and is_list(right) do if Inspect.List.printable?(left) and Inspect.List.printable?(right) do left = List.to_string(left) |> Inspect.BitString.escape(?') right = List.to_string(right) |> Inspect.BitString.escape(?') @@ -47,44 +53,33 @@ defmodule ExUnit.Diff do length2 = String.length(left) if bag_distance(left, right) / max(length1, length2) <= 0.6 do - "'" <> format_string(left, right, formatter) <> "'" + [{:eq, "'"}, script_string(left, right), {:eq, "'"}] end else keyword? = Inspect.List.keyword?(left) and Inspect.List.keyword?(right) - format_list(left, right, formatter, keyword?, []) + script_list(left, right, keyword?, []) end end # Numbers - def format(left, right, formatter) + def script(left, right) when is_integer(left) and is_integer(right) when is_float(left) and is_float(right) do - {kind, skew} = - case to_string(right - left) do - "-" <> _ = result -> - {:diff_delete, result} - result -> - {:diff_insert, "+" <> result} - end - value_diff = formatter.(kind, "(off by " <> skew <> ")") - format_string(inspect(left), inspect(right), formatter) <> " " <> value_diff + script_string(inspect(left), inspect(right)) end # Tuples - def format(left, right, formatter) + def script(left, right) when is_tuple(left) and is_tuple(right) do left = {left, tuple_size(left) - 1} right = {right, tuple_size(right) - 1} - format_tuple(left, right, formatter, []) + script_tuple(left, right, []) end - def format(_left, _right, _formatter), do: nil + def script(_left, _right), do: nil - defp format_string(string1, string2, formatter) do - string1 - |> String.myers_difference(string2) - |> Enum.map(&format_fragment(&1, formatter)) - |> IO.iodata_to_binary + defp script_string(string1, string2) do + String.myers_difference(string1, string2) end # The algorithm is outlined in the @@ -125,69 +120,68 @@ defmodule ExUnit.Diff do end) end - defp format_list([], [], _formatter, _keyword?, acc) do - result = with ", " <> rest <- Enum.join(Enum.reverse(acc)), do: rest - "[" <> result <> "]" + defp script_list([], [], _keyword?, acc) do + [[_ | elem_diff] | rest] = Enum.reverse(acc) + [{:eq, "["}, [elem_diff | rest], {:eq, "]"}] end - defp format_list([], [elem | rest], formatter, keyword?, acc) do - elem_diff = formatter.(:diff_insert, format_list_elem(elem, keyword?)) - format_list([], rest, formatter, keyword?, [", " <> elem_diff | acc]) + defp script_list([], [elem | rest], keyword?, acc) do + elem_diff = [ins: format_list_elem(elem, keyword?)] + script_list([], rest, keyword?, [[ins: ", "] ++ elem_diff | acc]) end - defp format_list([elem | rest], [], formatter, keyword?, acc) do - elem_diff = formatter.(:diff_delete, format_list_elem(elem, keyword?)) - format_list(rest, [], formatter, keyword?, [", " <> elem_diff | acc]) + defp script_list([elem | rest], [], keyword?, acc) do + elem_diff = [del: format_list_elem(elem, keyword?)] + script_list(rest, [], keyword?, [[del: ", "] ++ elem_diff | acc]) end - defp format_list([elem | rest1], [elem | rest2], formatter, keyword?, acc) do - elem_diff = format_list_elem(elem, keyword?) - format_list(rest1, rest2, formatter, keyword?, [", " <> elem_diff | acc]) + defp script_list([elem | rest1], [elem | rest2], keyword?, acc) do + elem_diff = [eq: format_list_elem(elem, keyword?)] + script_list(rest1, rest2, keyword?, [[eq: ", "] ++ elem_diff | acc]) end - defp format_list([{key1, val1} | rest1], [{key2, val2} | rest2], formatter, true, acc) do + defp script_list([{key1, val1} | rest1], [{key2, val2} | rest2], true, acc) do key_diff = if key1 != key2 do - format_string(Atom.to_string(key1), Atom.to_string(key2), formatter) + script_string(Atom.to_string(key1), Atom.to_string(key2)) else - Atom.to_string(key1) + [eq: Atom.to_string(key1)] end - value_diff = format_inner(val1, val2, formatter) - elem_diff = format_key_value(key_diff, value_diff, true) - format_list(rest1, rest2, formatter, true, [", " <> elem_diff | acc]) + value_diff = script_inner(val1, val2) + elem_diff = [[key_diff, {:eq, ": "}], value_diff] + script_list(rest1, rest2, true, [[eq: ", "] ++ elem_diff | acc]) end - defp format_list([elem1 | rest1], [elem2 | rest2], formatter, false, acc) do - elem_diff = format_inner(elem1, elem2, formatter) - format_list(rest1, rest2, formatter, false, [", " <> elem_diff | acc]) + defp script_list([elem1 | rest1], [elem2 | rest2], false, acc) do + elem_diff = script_inner(elem1, elem2) + script_list(rest1, rest2, false, [[eq: ", "] ++ elem_diff | acc]) end - defp format_list(last, [elem | rest], formatter, keyword?, acc) do - joiner_diff = format_plain_diff(" |", ",", formatter) <> " " - elem_diff = format_inner(last, elem, formatter) - new_acc = [joiner_diff <> elem_diff | acc] - format_list([], rest, formatter, keyword?, new_acc) + defp script_list(last, [elem | rest], keyword?, acc) do + joiner_diff = [del: " |", ins: ",", eq: " "] + elem_diff = script_inner(last, elem) + new_acc = [joiner_diff ++ elem_diff | acc] + script_list([], rest, keyword?, new_acc) end - defp format_list([elem | rest], last, formatter, keyword?, acc) do - joiner_diff = format_plain_diff(",", " |", formatter) <> " " - elem_diff = format_inner(elem, last, formatter) - new_acc = [joiner_diff <> elem_diff | acc] - format_list(rest, [], formatter, keyword?, new_acc) + defp script_list([elem | rest], last, keyword?, acc) do + joiner_diff = [del: ",", ins: " |", eq: " "] + elem_diff = script_inner(elem, last) + new_acc = [joiner_diff ++ elem_diff | acc] + script_list(rest, [], keyword?, new_acc) end - defp format_list(last1, last2, formatter, keyword?, acc) do + defp script_list(last1, last2, keyword?, acc) do elem_diff = cond do last1 == [] -> - formatter.(:diff_insert, inspect(last2)) + [ins: " | " <> inspect(last2)] last2 == [] -> - formatter.(:diff_delete, inspect(last1)) + [del: " | " <> inspect(last1)] true -> - format_inner(last1, last2, formatter) + [eq: " | "] ++ script_inner(last1, last2) end - new_acc = [" | " <> elem_diff | acc] - format_list([], [], formatter, keyword?, new_acc) + script_list([], [], keyword?, [elem_diff | acc]) end defp format_list_elem(elem, false), do: inspect(elem) @@ -196,74 +190,85 @@ defmodule ExUnit.Diff do format_key_value(Atom.to_string(key), inspect(val), true) end - defp format_tuple({_tuple1, -1}, {_tuple2, -1}, _formatter, acc) do - "{" <> Enum.join(acc, ", ") <> "}" + defp script_tuple({_tuple1, -1}, {_tuple2, -1}, acc) do + [[_ | elem_diff] | rest] = acc + [{:eq, "{"}, [elem_diff | rest], {:eq, "}"}] end - defp format_tuple({tuple1, index1}, {_, index2} = right, formatter, acc) + defp script_tuple({tuple1, index1}, {_, index2} = right, acc) when index1 > index2 do elem = elem(tuple1, index1) - elem_diff = formatter.(:diff_delete, inspect(elem)) - format_tuple({tuple1, index1 - 1}, right, formatter, [elem_diff | acc]) + elem_diff = [del: ", ", del: inspect(elem)] + script_tuple({tuple1, index1 - 1}, right, [elem_diff | acc]) end - defp format_tuple({_, index1} = left, {tuple2, index2}, formatter, acc) + defp script_tuple({_, index1} = left, {tuple2, index2}, acc) when index1 < index2 do elem = elem(tuple2, index2) - elem_diff = formatter.(:diff_insert, inspect(elem)) - format_tuple(left, {tuple2, index2 - 1}, formatter, [elem_diff | acc]) + elem_diff = [ins: ", ", ins: inspect(elem)] + script_tuple(left, {tuple2, index2 - 1}, [elem_diff | acc]) end - defp format_tuple({tuple1, index}, {tuple2, index}, formatter, acc) do + defp script_tuple({tuple1, index}, {tuple2, index}, acc) do elem1 = elem(tuple1, index) elem2 = elem(tuple2, index) - elem_diff = format_inner(elem1, elem2, formatter) - format_tuple({tuple1, index - 1}, {tuple2, index - 1}, formatter, [elem_diff | acc]) + elem_diff = script_inner(elem1, elem2) + script_tuple({tuple1, index - 1}, {tuple2, index - 1}, [[eq: ", "] ++ elem_diff | acc]) end - defp format_map(left, right, name, formatter) do - {surplus, altered, missing} = map_difference(left, right) + defp script_map(left, right, name) do + {surplus, altered, missing, same} = map_difference(left, right) keyword? = Inspect.List.keyword?(surplus) and Inspect.List.keyword?(altered) and - Inspect.List.keyword?(missing) + Inspect.List.keyword?(missing) and + Inspect.List.keyword?(same) - result = - if map_size(right) > length(altered) + length(missing), - do: ["..."], - else: [] + result = Enum.reduce(same, [], fn({key, val}, acc) -> + map_pair = format_key_value(inspect(key), inspect(val), keyword?) + [[eq: ", ", eq: map_pair] | acc] + end) result = Enum.reduce(missing, result, fn({key, val}, acc) -> map_pair = format_key_value(inspect(key), inspect(val), keyword?) - [formatter.(:diff_insert, map_pair) | acc] + [[ins: ", ", ins: map_pair] | acc] end) result = Enum.reduce(surplus, result, fn({key, val}, acc) -> map_pair = format_key_value(inspect(key), inspect(val), keyword?) - [formatter.(:diff_delete, map_pair) | acc] + [[del: ", ", del: map_pair] | acc] end) result = Enum.reduce(altered, result, fn({key, {val1, val2}}, acc) -> - value_diff = format_inner(val1, val2, formatter) - [format_key_value(inspect(key), value_diff, keyword?) | acc] + value_diff = script_inner(val1, val2) + [[{:eq, ", "}, script_key(key, keyword?), value_diff] | acc] end) - "%" <> name <> "{" <> Enum.join(result, ", ") <> "}" + [[_ | elem_diff] | rest] = result + [{:eq, "%" <> name <> "{"}, [elem_diff | rest], {:eq, "}"}] end defp map_difference(map1, map2) do - {surplus, altered} = - Enum.reduce(map1, {[], []}, fn({key, val1}, {surplus, altered} = acc) -> + {surplus, altered, same} = + Enum.reduce(map1, {[], [], []}, fn({key, val1}, {surplus, altered, same}) -> case Map.fetch(map2, key) do {:ok, ^val1} -> - acc + {surplus, altered, [{key, val1} | same]} {:ok, val2} -> - {surplus, [{key, {val1, val2}} | altered]} + {surplus, [{key, {val1, val2}} | altered], same} :error -> - {[{key, val1} | surplus], altered} + {[{key, val1} | surplus], altered, same} end end) missing = Enum.reduce(map2, [], fn({key, _} = pair, acc) -> if Map.has_key?(map1, key), do: acc, else: [pair | acc] end) - {surplus, altered, missing} + {surplus, altered, missing, same} + end + + defp script_key(key, false) do + [eq: inspect(key) <> " => "] + end + + defp script_key(key, true) do + [eq: Atom.to_string(key) <> ": "] end defp format_key_value(key, value, false) do @@ -278,32 +283,15 @@ defmodule ExUnit.Diff do key <> ": " <> value end - defp format_inner(term, term, _formatter), do: inspect(term) + defp script_inner(term, term) do + [eq: inspect(term)] + end - defp format_inner(left, right, formatter) do - if result = format(left, right, formatter) do + defp script_inner(left, right) do + if result = script(left, right) do result else - format_plain_diff(inspect(left), inspect(right), formatter) + [del: inspect(left), ins: inspect(right)] end end - - defp format_plain_diff(left, right, formatter) do - formatter.(:diff_delete, left) <> - formatter.(:diff_insert, right) - end - - defp format_fragment({:eq, content}, _), do: content - - defp format_fragment({:ins, content}, formatter) do - formatter.(:diff_insert, point_whitespace(content)) - end - - defp format_fragment({:del, content}, formatter) do - formatter.(:diff_delete, point_whitespace(content)) - end - - defp point_whitespace(content) do - String.replace(content, " ", "·") - end end diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index c3649c6e54c..869a9bceb22 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -119,24 +119,17 @@ defmodule ExUnit.Formatter do end @doc false - def format_assertion_error(%ExUnit.AssertionError{} = struct, width, formatter, counter_padding \\ @counter_padding) do + def format_assertion_error(%ExUnit.AssertionError{} = struct, width, formatter, counter_padding) do padding_size = byte_size(@inspect_padding) + inspect = &inspect_multiline(&1, padding_size, width) + {left, right} = format_sides(struct, formatter, inspect) - fields = [ + [ note: if_value(struct.message, &format_banner(&1, formatter)), code: if_value(struct.expr, &code_multiline(&1, padding_size)), - lhs: if_value(struct.left, &inspect_multiline(&1, padding_size, width)), - rhs: if_value(struct.right, &inspect_multiline(&1, padding_size, width)) + lhs: left, + rhs: right ] - - fields = - if formatter.(:diff_enabled?, nil) == true do - fields ++ [diff: format_diff(struct, formatter)] - else - fields - end - - fields |> filter_interesting_fields() |> format_each_field(formatter) |> make_into_lines(counter_padding) @@ -173,7 +166,7 @@ defmodule ExUnit.Formatter do end defp format_kind_reason(:error, %ExUnit.AssertionError{} = struct, width, formatter) do - format_assertion_error(struct, width, formatter) + format_assertion_error(struct, width, formatter, @counter_padding) end defp format_kind_reason(kind, reason, _width, formatter) do @@ -181,9 +174,7 @@ defmodule ExUnit.Formatter do end defp filter_interesting_fields(fields) do - Enum.filter(fields, fn {_, value} -> - value != ExUnit.AssertionError.no_value - end) + Enum.filter(fields, fn {_, value} -> has_value?(value) end) end defp format_each_field(fields, formatter) do @@ -193,10 +184,10 @@ defmodule ExUnit.Formatter do end defp if_value(value, fun) do - if value == ExUnit.AssertionError.no_value do - value - else + if has_value?(value) do fun.(value) + else + value end end @@ -231,18 +222,49 @@ defmodule ExUnit.Formatter do padding <> Enum.join(reasons, "\n" <> padding) <> "\n" end - defp format_diff(struct, formatter) do - if_value(struct.left, fn left -> - if_value(struct.right, fn right -> - format_diff(left, right, formatter) || ExUnit.AssertionError.no_value - end) - end) + defp format_sides(struct, formatter, inspect) do + left = struct.left + right = struct.right + case format_diff(left, right, formatter) do + {left, right} -> + {IO.iodata_to_binary(left), IO.iodata_to_binary(right)} + nil -> + {if_value(left, inspect), if_value(right, inspect)} + end + end + + defp has_value?(value) do + value != ExUnit.AssertionError.no_value end defp format_diff(left, right, formatter) do - task = Task.async(ExUnit.Diff, :format, [left, right, formatter]) + if has_value?(left) and has_value?(right) and formatter.(:diff_enabled?, false) do + if script = edit_script(left, right) do + colorize_diff(script, formatter, {[], []}) + end + end + end + + defp colorize_diff(script, formatter, acc) when is_list(script) do + Enum.reduce(script, acc, &colorize_diff(&1, formatter, &2)) + end + + defp colorize_diff({:eq, content}, _formatter, {left, right}) do + {[left | content], [right | content]} + end + + defp colorize_diff({:del, content}, formatter, {left, right}) do + {[left | formatter.(:diff_delete, content)], right} + end + + defp colorize_diff({:ins, content}, formatter, {left, right}) do + {left, [right | formatter.(:diff_insert, content)]} + end + + defp edit_script(left, right) do + task = Task.async(ExUnit.Diff, :script, [left, right]) case Task.yield(task, 1_500) || Task.shutdown(task, :brutal_kill) do - {:ok, diff} -> diff + {:ok, script} -> script nil -> nil end end diff --git a/lib/ex_unit/test/ex_unit/diff_test.exs b/lib/ex_unit/test/ex_unit/diff_test.exs index af172d6ef0b..0cb0d513982 100644 --- a/lib/ex_unit/test/ex_unit/diff_test.exs +++ b/lib/ex_unit/test/ex_unit/diff_test.exs @@ -5,14 +5,6 @@ defmodule ExUnit.DiffTest do import ExUnit.Diff - defp formatter(:diff_insert, message) do - "[" <> message <> "]" - end - - defp formatter(:diff_delete, message) do - "{" <> message <> "}" - end - defmodule User do defstruct [:age] end @@ -20,86 +12,149 @@ defmodule ExUnit.DiffTest do test "numbers" do int1 = 491512235 int2 = 490512035 - assert format(int1, int2, &formatter/2) == "49{1}[0]512{2}[0]35 {(off by -1000200)}" - assert format(42.0, 43.0, &formatter/2) == "4{2}[3].0 [(off by +1.0)]" - assert format(int1, 43.0, &formatter/2) == nil + expected = [eq: "49", del: "1", ins: "0", eq: "512", del: "2", ins: "0", eq: "35"] + assert script(int1, int2) == expected + assert script(42.0, 43.0) == [eq: "4", del: "2", ins: "3", eq: ".0"] + assert script(int1, 43.0) == nil end test "strings" do string1 = "fox hops over \"the dog" string2 = "fox jumps over the lazy cat" - expected = ~S<"fox {ho}[jum]ps over {\"}the {dog}[lazy·cat]"> - assert format(string1, string2, &formatter/2) == expected - assert format(string1, <<193, 31>>, &formatter/2) == nil - - # Filtered due to bag different - assert format("aaa", "bba", &formatter/2) == nil - assert format("aaa", "baa", &formatter/2) == "\"[b]aa{a}\"" + expected = [ + {:eq, "\""}, + [eq: "fox ", del: "ho", ins: "jum", eq: "ps over ", del: "\\\"", eq: "the ", del: "dog", ins: "lazy cat"], + {:eq, "\""} + ] + assert script(string1, string2) == expected + assert script(string1, <<193, 31>>) == nil + + # Filtered due to bag distance + assert script("aaa", "bba") == nil + assert script("aaa", "baa") == [{:eq, "\""}, [ins: "b", eq: "aa", del: "a"], {:eq, "\""}] + + assert script("", "") == [eq: "\"\""] end test "lists" do list1 = ["One", :ok, nil, {}, :ok] list2 = ["Two", :ok, 0.0, {true}] - expected = ~S<[{"One"}["Two"], :ok, {nil}[0.0], {[true]}, {:ok}]> - assert format(list1, list2, &formatter/2) == expected + expected = [ + {:eq, "["}, + [ + [del: "\"One\"", ins: "\"Two\""], + [eq: ", ", eq: ":ok"], + [eq: ", ", del: "nil", ins: "0.0"], + [{:eq, ", "}, {:eq, "{"}, [[ins: "true"]], {:eq, "}"}], + [del: ", ", del: ":ok"] + ], + {:eq, "]"} + ] + assert script(list1, list2) == expected keyword_list1 = [file: "nofile", line: 12] keyword_list2 = [file: nil, llne: 10] - expected = ~S<[file: {"nofile"}[nil], l{i}[l]ne: 1{2}[0] {(off by -2)}]> - assert format(keyword_list1, keyword_list2, &formatter/2) == expected + expected = [ + {:eq, "["}, + [ + [[[eq: "file"], {:eq, ": "}], [del: "\"nofile\"", ins: "nil"]], + [{:eq, ", "}, [[eq: "l", del: "i", ins: "l", eq: "ne"], {:eq, ": "}], [eq: "1", del: "2", ins: "0"]] + ], + {:eq, "]"} + ] + assert script(keyword_list1, keyword_list2) == expected charlist1 = 'fox hops over \'the dog' charlist2 = 'fox jumps over the lazy cat' - expected = "'fox {ho}[jum]ps over {\\'}the {dog}[lazy·cat]'" - assert format(charlist1, charlist2, &formatter/2) == expected + expected = [ + {:eq, "'"}, + [eq: "fox ", del: "ho", ins: "jum", eq: "ps over ", del: "\\'", eq: "the ", del: "dog", ins: "lazy cat"], + {:eq, "'"} + ] + assert script(charlist1, charlist2) == expected + + assert script([], []) == [eq: "[]"] end test "improper lists" do - assert format([1, 2], [1, 2 | 3], &formatter/2) == "[1, 2 | [3]]" - assert format([1, 2 | 3], [1, 2], &formatter/2) == "[1, 2 | {3}]" - assert format([1, "a"], [1 | "b"], &formatter/2) == ~S<[1{,}[ |] {"a"}["b"]]> - assert format([1 | "b"], [1, "a"], &formatter/2) == ~S<[1{ |}[,] {"b"}["a"]]> - assert format([1 | 2], [1 | 3], &formatter/2) == "[1 | {2}[3] [(off by +1)]]" - assert format([1, 'b' | 3], [1, 'a' | 3], &formatter/2) == "[1, {'b'}['a'] | 3]" - assert format(['a', 2 | 3], ['b', 2 | 3], &formatter/2) == "[{'a'}['b'], 2 | 3]" + expected = [{:eq, "["}, [[eq: "1"], [eq: ", ", eq: "2"], [ins: " | 3"]], {:eq, "]"}] + assert script([1, 2], [1, 2 | 3]) == expected + expected = [{:eq, "["}, [[eq: "1"], [eq: ", ", eq: "2"], [del: " | 3"]], {:eq, "]"}] + assert script([1, 2 | 3], [1, 2]) == expected + + expected = [{:eq, "["}, [[eq: "1"], [del: ",", ins: " |", eq: " ", del: "\"a\"", ins: "\"b\""]], {:eq, "]"}] + assert script([1, "a"], [1 | "b"]) == expected + expected = [{:eq, "["}, [[eq: "1"], [del: " |", ins: ",", eq: " ", del: "\"b\"", ins: "\"a\""]], {:eq, "]"}] + assert script([1 | "b"], [1, "a"]) == expected + + expected = [{:eq, "["}, [[eq: "1"], [eq: " | ", del: "2", ins: "3"]], {:eq, "]"}] + assert script([1 | 2], [1 | 3]) == expected + + expected = [{:eq, "["}, [[eq: "1"], [eq: ", ", del: "'b'", ins: "'a'"], [eq: " | ", eq: "3"]], {:eq, "]"}] + assert script([1, 'b' | 3], [1, 'a' | 3]) == expected + expected = [{:eq, "["}, [[del: "'a'", ins: "'b'"], [eq: ", ", eq: "2"], [eq: " | ", eq: "3"]], {:eq, "]"}] + assert script(['a', 2 | 3], ['b', 2 | 3]) == expected end test "tuples" do tuple1 = {:hex, '1.1'} tuple2 = {:hex, '0.1', [{:ex_doc}]} - expected = "{:hex, '{1}[0].1', [[{:ex_doc}]]}" - assert format(tuple1, tuple2, &formatter/2) == expected - assert format(tuple1, {}, &formatter/2) == "{{:hex}, {'1.1'}}" - assert format({}, tuple1, &formatter/2) == "{[:hex], ['1.1']}" + expected = [ + {:eq, "{"}, + [[eq: ":hex"], [{:eq, ", "}, {:eq, "'"}, [del: "1", ins: "0", eq: ".1"], {:eq, "'"}],[ins: ", ", ins: "[{:ex_doc}]"]], + {:eq, "}"} + ] + assert script(tuple1, tuple2) == expected + assert script(tuple1, {}) == [{:eq, "{"}, [[del: ":hex"], [del: ", ", del: "'1.1'"]], {:eq, "}"}] + assert script({}, tuple1) == [{:eq, "{"}, [[ins: ":hex"], [ins: ", ", ins: "'1.1'"]], {:eq, "}"}] + + assert script({}, {}) == [eq: "{}"] end test "maps" do - map1 = Enum.into(1..40, %{}, &{&1, &1}) |> Map.delete(33) - map2 = Enum.reduce(5..10, map1, &Map.delete(&2, &1)) |> Map.put(33, 33) |> Map.put(23, 32) - expected = "%{23 => {2}3[2] [(off by +9)], {8 => 8}, {7 => 7}, {6 => 6}, {10 => 10}, {9 => 9}, {5 => 5}, [33 => 33], ...}" - assert format(map1, map2, &formatter/2) == expected + map1 = Enum.into(1..15, %{}, &{&1, &1}) |> Map.delete(13) + map2 = Enum.reduce(5..10, map1, &Map.delete(&2, &1)) |> Map.put(13, 13) |> Map.put(12, 32) + expected = [ + {:eq, "%{"}, + [ + [[eq: "12 => "], [del: "1", ins: "3", eq: "2"]], + [del: ", ", del: "5 => 5"], [del: ", ", del: "6 => 6"], [del: ", ", del: "7 => 7"], + [del: ", ", del: "8 => 8"], [del: ", ", del: "9 => 9"], [del: ", ", del: "10 => 10"], + [ins: ", ", ins: "13 => 13"], + [eq: ", ", eq: "1 => 1"], [eq: ", ", eq: "2 => 2"], [eq: ", ", eq: "3 => 3"], + [eq: ", ", eq: "4 => 4"], [eq: ", ", eq: "11 => 11"], [eq: ", ", eq: "14 => 14"], + [eq: ", ", eq: "15 => 15"], + ], + {:eq, "}"} + ] + assert script(map1, map2) == expected map1 = %{baz: 12} map2 = %{foo: 12, bar: 12, baz: 12} - assert format(map1, map2, &formatter/2) == "%{[bar: 12], [foo: 12], ...}" - assert format(map2, map1, &formatter/2) == "%{{bar: 12}, {foo: 12}, ...}" - assert format(map1, %{}, &formatter/2) == "%{{baz: 12}}" - assert format(%{}, map1, &formatter/2) == "%{[baz: 12]}" + expected = [{:eq, "%{"}, [[ins: "bar: 12"], [ins: ", ", ins: "foo: 12"], [eq: ", ", eq: "baz: 12"]], {:eq, "}"}] + assert script(map1, map2) == expected + expected = [{:eq, "%{"}, [[del: "bar: 12"], [del: ", ", del: "foo: 12"], [eq: ", ", eq: "baz: 12"]], {:eq, "}"}] + assert script(map2, map1) == expected + assert script(map1, %{}) == [{:eq, "%{"}, [[del: "baz: 12"]], {:eq, "}"}] + assert script(%{}, map1) == [{:eq, "%{"}, [[ins: "baz: 12"]], {:eq, "}"}] + + assert script(%{}, %{}) == [eq: "%{}"] end test "structs" do user1 = %User{age: 16} user2 = %User{age: 21} - assert format(user1, user2, &formatter/2) == "%ExUnit.DiffTest.User{age: [2]1{6} [(off by +5)]}" - assert format(%User{}, %{}, &formatter/2) == nil - assert format(%User{}, %ExUnit.Test{}, &formatter/2) == nil + expected = [{:eq, "%ExUnit.DiffTest.User{"}, [[[eq: "age: "], [ins: "2", eq: "1", del: "6"]]], {:eq, "}"}] + assert script(user1, user2) == expected + assert script(%User{}, %{}) == nil + assert script(%User{}, %ExUnit.Test{}) == nil end test "not supported" do bin1 = <<147, 1, 2, 31>> bin2 = <<193, 1, 31>> - assert format(bin1, bin2, &formatter/2) == nil - assert format(:foo, :bar, &formatter/2) == nil - assert format(:foo, "bar", &formatter/2) == nil + assert script(bin1, bin2) == nil + assert script(:foo, :bar) == nil + assert script(:foo, "bar") == nil end end