defmodule MST.DiffTest do use ExUnit.Case, async: true doctest MST.Diff alias DASL.CID alias MST.{Diff, Tree} defp new_tree, do: Tree.new(MST.Store.Memory.new()) defp val(s), do: CID.compute(s, :raw) describe "compute/2" do test "two empty trees produce empty diff" do assert {:ok, diff} = Diff.compute(new_tree(), new_tree()) assert MapSet.size(diff.created_nodes) == 0 assert MapSet.size(diff.deleted_nodes) == 0 assert diff.record_ops == [] end test "empty → non-empty: all keys are creates" do v = val("v") {:ok, tree_b} = Tree.put(new_tree(), "col/a", v) assert {:ok, diff} = Diff.compute(new_tree(), tree_b) assert length(diff.record_ops) == 1 op = hd(diff.record_ops) assert op.key == "col/a" assert op.old_value == nil assert op.new_value == v end test "non-empty → empty: all keys are deletes" do v = val("v") {:ok, tree_a} = Tree.put(new_tree(), "col/a", v) assert {:ok, diff} = Diff.compute(tree_a, new_tree()) assert length(diff.record_ops) == 1 op = hd(diff.record_ops) assert op.key == "col/a" assert op.old_value == v assert op.new_value == nil end test "identical trees produce empty diff" do v = val("v") {:ok, tree} = Tree.put(new_tree(), "col/a", v) assert {:ok, diff} = Diff.compute(tree, tree) assert diff.record_ops == [] assert MapSet.size(diff.created_nodes) == 0 assert MapSet.size(diff.deleted_nodes) == 0 end test "update: same key, different value" do v1 = val("v1") v2 = val("v2") {:ok, tree_a} = Tree.put(new_tree(), "col/a", v1) {:ok, tree_b} = Tree.put(new_tree(), "col/a", v2) assert {:ok, diff} = Diff.compute(tree_a, tree_b) assert length(diff.record_ops) == 1 op = hd(diff.record_ops) assert op.old_value == v1 assert op.new_value == v2 end test "no-op: same key, same value, different surrounding context" do v = val("v") v2 = val("v2") {:ok, tree_a} = Tree.put(new_tree(), "col/a", v) {:ok, tree_a} = Tree.put(tree_a, "col/b", v2) {:ok, tree_b} = Tree.put(new_tree(), "col/a", v) {:ok, tree_b} = Tree.put(tree_b, "col/c", v2) assert {:ok, diff} = Diff.compute(tree_a, tree_b) keys = Enum.map(diff.record_ops, & &1.key) refute "col/a" in keys assert "col/b" in keys assert "col/c" in keys end test "record_ops are sorted by key" do v = val("v") {:ok, tree_b} = Enum.reduce(["col/z", "col/a", "col/m"], new_tree(), fn k, acc -> {:ok, t} = Tree.put(acc, k, v) t end) |> then(&{:ok, &1}) assert {:ok, diff} = Diff.compute(new_tree(), tree_b) keys = Enum.map(diff.record_ops, & &1.key) assert keys == Enum.sort(keys) end test "created_nodes and deleted_nodes are non-overlapping for insert" do v = val("v") {:ok, tree_b} = Tree.put(new_tree(), "col/a", v) assert {:ok, diff} = Diff.compute(new_tree(), tree_b) assert MapSet.disjoint?(diff.created_nodes, diff.deleted_nodes) end test "multi-key add and remove" do v = val("v") va = val("va") {:ok, base} = Tree.put(new_tree(), "col/keep", v) {:ok, tree_a} = Tree.put(base, "col/remove", v) {:ok, tree_b} = Tree.put(base, "col/add", va) assert {:ok, diff} = Diff.compute(tree_a, tree_b) op_keys = Enum.map(diff.record_ops, & &1.key) |> MapSet.new() assert MapSet.member?(op_keys, "col/remove") assert MapSet.member?(op_keys, "col/add") refute MapSet.member?(op_keys, "col/keep") end end end