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_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_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_autoresolve_inline_source(): value = """\ def hello(): print("world!") """ le = op_patch("source", [op_replace(24, 'W')]) # FIXME: Character based here, should be linebased? re = op_patch("source", [op_replace(29, '.')]) expected = """\ <<<<<<< local def hello(): print("World!") ======= base def hello(): print("world!") ======= remote def hello(): print("world.") >>>>>>> """ actual = make_inline_source_value(value, le, re) print(actual) assert actual == expected
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_input_strategy_inline_source_conflict(): # Conflicting cell inserts at same location as removing old cell local = [["local\n", "some other\n", "lines\n", "to align\n"]] base = [["base\n", "some other\n", "lines\n", "to align\n"]] remote = [["remote\n", "some other\n", "lines\n", "to align\n"]] # Ideal case: if have_git: expected_partial = [[ "<<<<<<< local\n", "local\n", "=======\n", "remote\n", ">>>>>>> remote\n", "some other\n", "lines\n", "to align\n", ]] else: # Fallback is not very smart yet: expected_partial = [[ "<<<<<<< local\n", "local\n", "some other\n", "lines\n", "to align\n", #'||||||| base\n', #'base\n', #'some other\n', #'lines\n', #'to align\n', "=======\n", "remote\n", "some other\n", "lines\n", "to align\n", ">>>>>>> remote", ]] expected_conflicts = [{ "common_path": ("cells", 0, "source"), "local_diff": [op_addrange(0, local[0][0:1]), op_removerange(0, 1)], "remote_diff": [op_addrange(0, remote[0][0:1]), op_removerange(0, 1)], "custom_diff": [op_addrange(0, expected_partial[0]), op_removerange(0, len(base[0]))], }] expected_conflicts = [{ "common_path": ("cells", 0), "conflict": True, "action": "custom", "local_diff": [ op_patch("source", [op_addrange(0, local[0][0:1]), op_removerange(0, 1)]) ], "remote_diff": [ op_patch("source", [op_addrange(0, remote[0][0:1]), op_removerange(0, 1)]) ], "custom_diff": [op_replace("source", "".join(expected_partial[0]))], }] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "use-base" merge_args.input_strategy = "inline" _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
def test_merge_input_strategy_inline_source_conflict(): # Conflicting cell inserts at same location as removing old cell local = [["local\n", "some other\n", "lines\n", "to align\n"]] base = [["base\n", "some other\n", "lines\n", "to align\n"]] remote = [["remote\n", "some other\n", "lines\n", "to align\n"]] # Ideal case: if have_git: expected_partial = [[ "<<<<<<< local\n", "local\n", "=======\n", "remote\n", ">>>>>>> remote\n", "some other\n", "lines\n", "to align\n", ]] else: # Fallback is not very smart yet: expected_partial = [[ "<<<<<<< local\n", "local\n", "some other\n", "lines\n", "to align\n", #'||||||| base\n', #'base\n', #'some other\n', #'lines\n', #'to align\n', "=======\n", "remote\n", "some other\n", "lines\n", "to align\n", ">>>>>>> remote", ]] expected_conflicts = [{ "common_path": ("cells", 0, "source"), "local_diff": [ op_addrange(0, local[0][0:1]), op_removerange(0, 1) ], "remote_diff": [ op_addrange(0, remote[0][0:1]), op_removerange(0, 1) ], "custom_diff": [ op_addrange(0, expected_partial[0]), op_removerange(0, len(base[0])) ], }] expected_conflicts = [{ "common_path": ("cells", 0), "conflict": True, "action": "custom", "local_diff": [op_patch("source", [ op_addrange(0, local[0][0:1]), op_removerange(0, 1) ])], "remote_diff": [op_patch("source", [ op_addrange(0, remote[0][0:1]), op_removerange(0, 1) ])], "custom_diff": [op_replace("source", "".join(expected_partial[0]))], }] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "use-base" merge_args.input_strategy = "inline" _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
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_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_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) ]), ]