def test_patch_dict(): # Test +, single item insertion assert patch({}, [op_add("d", 4)]) == {"d": 4} assert patch({"a": 1}, [op_add("d", 4)]) == {"a": 1, "d": 4} #assert patch({"d": 1}, [op_add("d", 4)]) == {"d": 4} # currently triggers assert, raise exception or allow? # Test -, single item deletion assert patch({"a": 1}, [op_remove("a")]) == {} assert patch({"a": 1, "b": 2}, [op_remove("a")]) == {"b": 2} # Test :, single item replace assert patch({"a": 1, "b": 2}, [op_replace("a", 3)]) == {"a": 3, "b": 2} assert patch({ "a": 1, "b": 2 }, [op_replace("a", 3), op_replace("b", 5)]) == { "a": 3, "b": 5 } # Test !, item patch subdiff = [ op_patch(0, [op_patch(0, [op_replace(0, "H")])]), op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])]) ] assert patch({ "a": ["hello", "world"], "b": 3 }, [op_patch("a", subdiff)]) == { "a": ["Hello", "World"], "b": 3 }
def test_pop_patch_unpoppable(): md = MergeDecision(common_path=("a", "b"), action="base", conflict=True, local_diff=[op_remove("c")], remote_diff=[op_patch("c", [op_remove("d")])]) dec = pop_patch_decision(md) assert dec is None
def test_merge_builder_ensures_common_path(): b = MergeDecisionBuilder() b.conflict(("a", "b"), [op_patch("c", [op_remove("d")])], [op_patch("c", [op_remove("e")])]) assert len(b.decisions) == 1 assert b.decisions[0].common_path == ("a", "b", "c") assert b.decisions[0].local_diff == [op_remove("d")] assert b.decisions[0].remote_diff == [op_remove("e")]
def test_pop_patch_unpoppable(): md = MergeDecision( common_path=("a", "b"), action="base", conflict=True, local_diff=[op_remove("c")], remote_diff=[op_patch("c", [op_remove("d")])] ) dec = pop_patch_decision(md) assert dec is None
def test_pop_patch_single_level(): md = MergeDecision(common_path=("a", "b"), action="base", conflict=True, local_diff=[op_patch("c", [op_remove("d")])], remote_diff=[op_patch("c", [op_remove("e")])]) dec = pop_patch_decision(md) assert dec is not None assert dec.common_path == ("a", "b", "c") assert dec.local_diff == [op_remove("d")] assert dec.remote_diff == [op_remove("e")]
def test_pop_patch_single_level(): md = MergeDecision( common_path=("a", "b"), action="base", conflict=True, local_diff=[op_patch("c", [op_remove("d")])], remote_diff=[op_patch("c", [op_remove("e")])] ) dec = pop_patch_decision(md) assert dec is not None assert dec.common_path == ("a", "b", "c") assert dec.local_diff == [op_remove("d")] assert dec.remote_diff == [op_remove("e")]
def test_patch_list(): # Test +, single item insertion assert patch([], [op_add(0, 3)]) == [3] assert patch([], [op_add(0, 3), op_add(0, 4)]) == [3, 4] assert patch([], [op_add(0, 3), op_add(0, 4), op_add(0, 5)]) == [3, 4, 5] # Test -, single item deletion assert patch([3], [op_remove(0)]) == [] assert patch([5, 6, 7], [op_remove(0)]) == [6, 7] assert patch([5, 6, 7], [op_remove(1)]) == [5, 7] assert patch([5, 6, 7], [op_remove(2)]) == [5, 6] assert patch([5, 6, 7], [op_remove(0), op_remove(2)]) == [6] # Test :, single item replace pass # Test !, item patch assert patch(["hello", "world"], [op_patch(0, [op_patch(0, [op_replace(0, "H")])]), op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])]) == ["Hello", "World"] # Test ++, sequence insertion assert patch([], [op_addrange(0, [3, 4]), op_add(0, 5), op_addrange(0, [6, 7])]) == [3, 4, 5, 6, 7] # Test --, sequence deletion assert patch([5, 6, 7, 8], [op_removerange(0, 2)]) == [7, 8] assert patch([5, 6, 7, 8], [op_removerange(1, 2)]) == [5, 8] assert patch([5, 6, 7, 8], [op_removerange(2, 2)]) == [5, 6]
def test_patch_str(): # Test +, single item insertion assert patch("42", [op_patch(0, [op_add(0, "3"), op_remove(1)])]) == "34" # Test -, single item deletion assert patch("3", [op_patch(0, [op_remove(0)])]) == "" assert patch("42", [op_patch(0, [op_remove(0)])]) == "2" assert patch("425", [op_patch(0, [op_remove(0)])]) == "25" assert patch("425", [op_patch(0, [op_remove(1)])]) == "45" assert patch("425", [op_patch(0, [op_remove(2)])]) == "42" # Test :, single item replace assert patch("52", [op_patch(0, [op_replace(0, "4")])]) == "42" assert patch("41", [op_patch(0, [op_replace(1, "2")])]) == "42" assert patch("42", [op_patch(0, [op_replace(0, "3"), op_replace(1, "5")])]) == "35" assert patch("hello", [op_patch(0, [op_replace(0, "H")])]) == "Hello" # Replace by delete-then-insert assert patch("world", [op_patch(0, [op_remove(0), op_add(0, "W")])]) == "World" # Test !, item patch (doesn't make sense for str) pass # Test ++, sequence insertion assert patch("", [op_patch(0, [op_addrange(0, "34"), op_add(0, "5"), op_addrange(0, "67")])]) == "34567" # Test --, sequence deletion assert patch("abcd", [op_patch(0, [op_removerange(0, 2)])]) == "cd" assert patch("abcd", [op_patch(0, [op_removerange(1, 2)])]) == "ad" assert patch("abcd", [op_patch(0, [op_removerange(2, 2)])]) == "ab"
def test_patch_str(): # Test +, single item insertion assert patch("42", [op_patch(0, [op_add(0, "3"), op_remove(1)])]) == "34" # Test -, single item deletion assert patch("3", [op_patch(0, [op_remove(0)])]) == "" assert patch("42", [op_patch(0, [op_remove(0)])]) == "2" assert patch("425", [op_patch(0, [op_remove(0)])]) == "25" assert patch("425", [op_patch(0, [op_remove(1)])]) == "45" assert patch("425", [op_patch(0, [op_remove(2)])]) == "42" # Test :, single item replace assert patch("52", [op_patch(0, [op_replace(0, "4")])]) == "42" assert patch("41", [op_patch(0, [op_replace(1, "2")])]) == "42" assert patch("42", [op_patch(0, [op_replace(0, "3"), op_replace(1, "5")])]) == "35" assert patch("hello", [op_patch(0, [op_replace(0, "H")])]) == "Hello" # Replace by delete-then-insert assert patch("world", [op_patch(0, [op_remove(0), op_add(0, "W")])]) == "World" # Test !, item patch (doesn't make sense for str) pass # Test ++, sequence insertion assert patch("", [op_patch(0, [op_addrange( 0, "34"), op_add(0, "5"), op_addrange( 0, "67")])]) == "34567" # Test --, sequence deletion assert patch("abcd", [op_patch(0, [op_removerange(0, 2)])]) == "cd" assert patch("abcd", [op_patch(0, [op_removerange(1, 2)])]) == "ad" assert patch("abcd", [op_patch(0, [op_removerange(2, 2)])]) == "ab"
def test_patch_list(): # Test +, single item insertion assert patch([], [op_add(0, 3)]) == [3] assert patch([], [op_add(0, 3), op_add(0, 4)]) == [3, 4] assert patch([], [op_add(0, 3), op_add(0, 4), op_add(0, 5)]) == [3, 4, 5] # Test -, single item deletion assert patch([3], [op_remove(0)]) == [] assert patch([5, 6, 7], [op_remove(0)]) == [6, 7] assert patch([5, 6, 7], [op_remove(1)]) == [5, 7] assert patch([5, 6, 7], [op_remove(2)]) == [5, 6] assert patch([5, 6, 7], [op_remove(0), op_remove(2)]) == [6] # Test :, single item replace pass # Test !, item patch assert patch(["hello", "world"], [ op_patch(0, [op_patch(0, [op_replace(0, "H")])]), op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])]) ]) == ["Hello", "World"] # Test ++, sequence insertion assert patch( [], [op_addrange(0, [3, 4]), op_add(0, 5), op_addrange(0, [6, 7])]) == [3, 4, 5, 6, 7] # Test --, sequence deletion assert patch([5, 6, 7, 8], [op_removerange(0, 2)]) == [7, 8] assert patch([5, 6, 7, 8], [op_removerange(1, 2)]) == [5, 8] assert patch([5, 6, 7, 8], [op_removerange(2, 2)]) == [5, 6]
def test_patch_dict(): # Test +, single item insertion assert patch({}, [op_add("d", 4)]) == {"d": 4} assert patch({"a": 1}, [op_add("d", 4)]) == {"a": 1, "d": 4} #assert patch({"d": 1}, [op_add("d", 4)]) == {"d": 4} # currently triggers assert, raise exception or allow? # Test -, single item deletion assert patch({"a": 1}, [op_remove("a")]) == {} assert patch({"a": 1, "b": 2}, [op_remove("a")]) == {"b": 2} # Test :, single item replace assert patch({"a": 1, "b": 2}, [op_replace("a", 3)]) == {"a": 3, "b": 2} assert patch({"a": 1, "b": 2}, [op_replace("a", 3), op_replace("b", 5)]) == {"a": 3, "b": 5} # Test !, item patch subdiff = [op_patch(0, [op_patch(0, [op_replace(0, "H")])]), op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])] assert patch({"a": ["hello", "world"], "b": 3}, [op_patch("a", subdiff)]) == {"a": ["Hello", "World"], "b": 3}
def test_build_diffs_unsorted(): b = MergeDecisionBuilder() b.onesided((), [op_remove('a')], None) b.onesided(('b',), [op_remove('j')], None) b.onesided(('c',), [op_remove('k')], None) b.onesided(('d',), [op_remove('l')], None) base = dict(a=1, b=dict(i=2), c=dict(j=3), d=dict(k=4)) diff = build_diffs(base, b.decisions, 'local') assert len(diff) == 4 assert diff[0] == op_remove('a') assert diff[1] == op_patch('b', [op_remove('j')]) assert diff[2] == op_patch('c', [op_remove('k')]) assert diff[3] == op_patch('d', [op_remove('l')])
def test_ensure_common_path_multilevel_intermediate(): diffs = [[op_patch("c", [op_patch("d", [op_remove("f")])])], [op_patch("c", [op_patch("e", [op_remove("g")])])]] res = ensure_common_path(("a", "b"), diffs) assert res == (("a", "b", "c"), [[op_patch("d", [op_remove("f")])], [op_patch("e", [op_remove("g")])]])
def test_merge_conflicting_nested_dicts(): # Note: Tests in here were written by writing up the last version # and then copy-pasting and deleting pieces to simplify... # Not pretty for production code but the explicitness is convenient when the tests fail. # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2}} r = {"a": {"x": 3}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}} assert lc == [op_patch("a", [op_replace("x", 2)]), ] assert rc == [op_patch("a", [op_replace("x", 3)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {}} l = {"a": {"y": 4}} r = {"a": {"y": 5}} m, lc, rc = merge(b, l, r) assert m == {"a": {}} assert lc == [op_patch("a", [op_add("y", 4)]), ] assert rc == [op_patch("a", [op_add("y", 5)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2, "y": 4}} r = {"a": {"x": 3, "y": 5}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}} assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]), ] assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, } m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}} assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), ] assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), op_patch("m", [op_replace("x", 17)]), ] assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), op_patch("m", [op_replace("x", 27)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}, "n": {"q": 9}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}, "n": {"q": 19}} m, lc, rc = merge(b, l, r) # Note that "n":{} gets added to the merge result even though it's empty assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}, "n": {}} assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), op_patch("m", [op_replace("x", 17)]), op_patch("n", [op_add("q", 9)]) ] assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), op_patch("m", [op_replace("x", 27)]), op_patch("n", [op_add("q", 19)]) ]
def test_ensure_common_path_one_sided_remote(): diffs = [[op_patch("c", [op_remove("d")])], []] res = ensure_common_path(("a", "b"), diffs) assert res == (("a", "b", "c"), [[op_remove("d")], None])
def test_ensure_common_path_one_sided_empty(): diffs = [[], [op_patch("c", [op_remove("d")])]] res = ensure_common_path(("a", "b"), diffs) assert res == (("a", "b", "c"), [None, [op_remove("d")]])
def test_ensure_common_path_single_level(): diffs = [[op_patch("c", [op_remove("e")])], [op_patch("c", [op_remove("d")])]] res = ensure_common_path(("a", "b"), diffs) assert res == (("a", "b", "c"), [[op_remove("e")], [op_remove("d")]])
def test_ensure_common_path_no_change(): diffs = [[op_remove("c")], [op_patch("c", [op_remove("d")])]] res = ensure_common_path(("a", "b"), diffs) assert res == (("a", "b"), diffs)
def test_merge_conflicting_nested_dicts(): # Note: Tests in here were written by writing up the last version # and then copy-pasting and deleting pieces to simplify... # Not pretty for production code but the explicitness is convenient when the tests fail. # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2}} r = {"a": {"x": 3}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}} assert lc == [ op_patch("a", [op_replace("x", 2)]), ] assert rc == [ op_patch("a", [op_replace("x", 3)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {}} l = {"a": {"y": 4}} r = {"a": {"y": 5}} m, lc, rc = merge(b, l, r) assert m == {"a": {}} assert lc == [ op_patch("a", [op_add("y", 4)]), ] assert rc == [ op_patch("a", [op_add("y", 5)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2, "y": 4}} r = {"a": {"x": 3, "y": 5}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}} assert lc == [ op_patch("a", [op_replace("x", 2), op_add("y", 4)]), ] assert rc == [ op_patch("a", [op_replace("x", 3), op_add("y", 5)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}} r = { "a": { "x": 3, "y": 5 }, "d": { "x": 5 }, } m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}} assert lc == [ op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), ] assert rc == [ op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}} m, lc, rc = merge(b, l, r) assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert lc == [ op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), op_patch("m", [op_replace("x", 17)]), ] assert rc == [ op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), op_patch("m", [op_replace("x", 27)]), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}, "n": {"q": 9}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}, "n": {"q": 19}} m, lc, rc = merge(b, l, r) # Note that "n":{} gets added to the merge result even though it's empty assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}, "n": {}} assert lc == [ op_patch("a", [op_replace("x", 2), op_add("y", 4)]), op_patch("d", [op_remove("x"), op_replace("y", 6)]), op_patch("m", [op_replace("x", 17)]), op_patch("n", [op_add("q", 9)]) ] assert rc == [ op_patch("a", [op_replace("x", 3), op_add("y", 5)]), op_patch("d", [op_replace("x", 5), op_remove("y")]), op_patch("m", [op_replace("x", 27)]), op_patch("n", [op_add("q", 19)]) ]
def test_diff_and_patch(): # Note: check_symmetric_diff_and_patch handles (a,b) and (b,a) for both # shallow and deep diffs, simplifying the number of cases to cover in here. # Empty mda = {} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty mda = {"a": 1} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty multilevel mda = {"a": 1, "b": {"ba": 21}} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty multilevel mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # Partial delete mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"b": {"ba": 21}, "c": {"cb": 32}} check_symmetric_diff_and_patch(mda, mdb) # One-level modification mda = {"a": 1} mdb = {"a": 10} check_symmetric_diff_and_patch(mda, mdb) # Two-level modification mda = {"a": 1, "b": {"ba": 21}} mdb = {"a": 10, "b": {"ba": 210}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}} mdb = {"a": 1, "b": {"ba": 210}} check_symmetric_diff_and_patch(mda, mdb) # Multilevel modification mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 10, "b": {"ba": 210}, "c": {"ca": 310, "cb": 320}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 1, "b": {"ba": 210}, "c": {"ca": 310, "cb": 32}} check_symmetric_diff_and_patch(mda, mdb) # Multilevel mix of delete, add, modify mda = {"deleted": 1, "modparent": {"mod": 21}, "mix": {"del": 31, "mod": 32, "unchanged": 123}} mdb = {"added": 7, "modparent": {"mod": 22}, "mix": {"add": 42, "mod": 37, "unchanged": 123}} check_symmetric_diff_and_patch(mda, mdb) # A more explicit assert showing the diff format and testing that paths are sorted: assert diff(mda, mdb) == [ op_add("added", 7), op_remove("deleted"), op_patch("mix", [ op_add("add", 42), op_remove("del"), op_replace("mod", 37), ]), op_patch("modparent", [ op_replace("mod", 22) ]), ]
def test_merge_conflicting_nested_dicts(): # Note: Tests in here were written by writing up the last version # and then copy-pasting and deleting pieces to simplify... # Not pretty for production code but the explicitness is convenient when the tests fail. # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2}} r = {"a": {"x": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}} assert len(decisions) == 1 d = decisions[0] assert d.common_path == ("a",) assert d.local_diff == [op_replace("x", 2)] assert d.remote_diff == [op_replace("x", 3)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {}} l = {"a": {"y": 4}} r = {"a": {"y": 5}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {}} assert len(decisions) == 1 d = decisions[0] assert d.common_path == ("a",) assert d.local_diff == [op_add("y", 4), ] assert d.remote_diff == [op_add("y", 5), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2, "y": 4}} r = {"a": {"x": 3, "y": 5}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}} assert len(decisions) == 2 assert all([d.conflict for d in decisions]) assert all([d.common_path == ("a",) for d in decisions]) assert decisions[0].local_diff == [op_replace("x", 2)] assert decisions[0].remote_diff == [op_replace("x", 3)] assert decisions[1].local_diff == [op_add("y", 4)] assert decisions[1].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, } decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}, "d": {"x": 4, "y": 5}} assert len(decisions) == 4 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("d",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("a",) assert decisions[3].common_path == ("a",) assert decisions[0].local_diff == [op_remove("x")] assert decisions[0].remote_diff == [op_replace("x", 5)] assert decisions[1].local_diff == [op_replace("y", 6)] assert decisions[1].remote_diff == [op_remove("y")] assert decisions[2].local_diff == [op_replace("x", 2)] assert decisions[2].remote_diff == [op_replace("x", 3)] assert decisions[3].local_diff == [op_add("y", 4)] assert decisions[3].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == { "a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert len(decisions) == 5 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("m",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("d",) assert decisions[3].common_path == ("a",) assert decisions[4].common_path == ("a",) assert decisions[0].local_diff == [op_replace("x", 17)] assert decisions[0].remote_diff == [op_replace("x", 27)] assert decisions[1].local_diff == [op_remove("x")] assert decisions[1].remote_diff == [op_replace("x", 5)] assert decisions[2].local_diff == [op_replace("y", 6)] assert decisions[2].remote_diff == [op_remove("y")] assert decisions[3].local_diff == [op_replace("x", 2)] assert decisions[3].remote_diff == [op_replace("x", 3)] assert decisions[4].local_diff == [op_add("y", 4)] assert decisions[4].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}, "n": {"q": 9}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}, "n": {"q": 19}} decisions = decide_merge(b, l, r) # Note that "n":{} gets added to the merge result even though it's empty assert apply_decisions(b, decisions) == { "a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert len(decisions) == 6 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("m",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("d",) assert decisions[3].common_path == ("a",) assert decisions[4].common_path == ("a",) assert decisions[5].common_path == () assert decisions[0].local_diff == [op_replace("x", 17)] assert decisions[0].remote_diff == [op_replace("x", 27)] assert decisions[1].local_diff == [op_remove("x")] assert decisions[1].remote_diff == [op_replace("x", 5)] assert decisions[2].local_diff == [op_replace("y", 6)] assert decisions[2].remote_diff == [op_remove("y")] assert decisions[3].local_diff == [op_replace("x", 2)] assert decisions[3].remote_diff == [op_replace("x", 3)] assert decisions[4].local_diff == [op_add("y", 4)] assert decisions[4].remote_diff == [op_add("y", 5)] assert decisions[5].local_diff == [op_add("n", {"q": 9})] assert decisions[5].remote_diff == [op_add("n", {"q": 19})]
def test_diff_and_patch(): # Note: check_symmetric_diff_and_patch handles (a,b) and (b,a) for both # shallow and deep diffs, simplifying the number of cases to cover in here. # Empty mda = {} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty mda = {"a": 1} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty multilevel mda = {"a": 1, "b": {"ba": 21}} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # One-sided content/empty multilevel mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {} check_symmetric_diff_and_patch(mda, mdb) # Partial delete mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"b": {"ba": 21}, "c": {"cb": 32}} check_symmetric_diff_and_patch(mda, mdb) # One-level modification mda = {"a": 1} mdb = {"a": 10} check_symmetric_diff_and_patch(mda, mdb) # Two-level modification mda = {"a": 1, "b": {"ba": 21}} mdb = {"a": 10, "b": {"ba": 210}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}} mdb = {"a": 1, "b": {"ba": 210}} check_symmetric_diff_and_patch(mda, mdb) # Multilevel modification mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 10, "b": {"ba": 210}, "c": {"ca": 310, "cb": 320}} check_symmetric_diff_and_patch(mda, mdb) mda = {"a": 1, "b": {"ba": 21}, "c": {"ca": 31, "cb": 32}} mdb = {"a": 1, "b": {"ba": 210}, "c": {"ca": 310, "cb": 32}} check_symmetric_diff_and_patch(mda, mdb) # Multilevel mix of delete, add, modify mda = { "deleted": 1, "modparent": { "mod": 21 }, "mix": { "del": 31, "mod": 32, "unchanged": 123 } } mdb = { "added": 7, "modparent": { "mod": 22 }, "mix": { "add": 42, "mod": 37, "unchanged": 123 } } check_symmetric_diff_and_patch(mda, mdb) # A more explicit assert showing the diff format and testing that paths are sorted: assert diff(mda, mdb) == [ op_add("added", 7), op_remove("deleted"), op_patch("mix", [ op_add("add", 42), op_remove("del"), op_replace("mod", 37), ]), op_patch("modparent", [op_replace("mod", 22)]), ]