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_merge_insert_cells_around_conflicting_cell(): # Modifying an original cell and inserting a new cell on both sides source = [ "def foo(x, y):\n", " z = x * y\n", " return z\n", ] local = [["new local cell\n"], source + ["local\n"]] base = [source] remote = [source + ["remote\n"], ["new remote cell\n"]] # Use mergetool strategy: merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" if 0: # This is how it would look if neither source or cell inserts resulted # in conflicts: expected_partial = [ local[0], source + ["local\n", "remote\n"], remote[1] ] expected_conflicts = [] elif 1: # This is how it would look if source inserts but not cell inserts # resulted in conflicts: expected_partial = [local[0], source, remote[1]] expected_conflicts = [{ "common_path": ("cells", 0, "source"), "local_diff": [op_addrange(len(source), ["local\n"])], "remote_diff": [op_addrange(len(source), ["remote\n"])] }] else: # In current behaviour: # - base cell 0 is aligned correctly (this is the notebook diff heuristics) # - conflicting edits of base cell 0 detected # - local insert before cell 0 is treated as part of conflict on base cell 0 # - remote insert after cell 0 is not treated as part of conflict # FIXME: This may not be the exact behaviour we want: # - For source, we might want both inserts to be part of the conflict. # (if so, fix in generic merge and chunk collection) # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?) # - Figure out the best behaviour and make it happen! expected_partial = [source, remote[1]] expected_conflicts = [{ "common_path": ("cells", ), "local_diff": [ op_addrange( 0, [nbformat.v4.new_code_cell(source=["new local cell"])]), op_patch(0, [ op_patch("source", [op_addrange(len("".join(source)), "local\n")]) ]), ], "remote_diff": [ op_patch(0, [ op_patch('source', [op_addrange(len("".join(source)), "remote\n")]) ]) ] }] _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
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_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_merge_inserts_within_deleted_range(): # Multiple inserts within a deleted range base = """ def f(x): return x**2 def g(x): return x + 2 """ # Insert foo and bar local = """ def foo(y): return y / 3 def f(x): return x**2 def bar(y): return y - 3 def g(x): return x + 2 """ remote = "" # Delete all if 0: # This is quite optimistic and would require employing aggressive # attempts at automatic resolution beyond what git and meld do: expected_partial = """ def foo(y): return y / 3 def bar(y): return y - 3 """ else: expected_partial = base expected_conflicts = [{ "common_path": ("cells", ), "local_diff": [ op_patch("cells", [ op_addrange(0, [nbformat.v4.new_code_cell(local)]), op_removerange(0, 1) ]) ], "remote_diff": [ op_patch("cells", [ op_addrange(0, [nbformat.v4.new_code_cell(remote)]), op_removerange(0, 1) ]) ] }] _check_sources(base, local, remote, expected_partial, expected_conflicts) # Keep it failing assert False
def test_merge_insert_cells_around_conflicting_cell(): # Modifying an original cell and inserting a new cell on both sides source = [ "def foo(x, y):\n", " z = x * y\n", " return z\n", ] local = [["new local cell\n"], source + ["local\n"]] base = [source] remote = [source + ["remote\n"], ["new remote cell\n"]] # Use mergetool strategy: merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" if 0: # This is how it would look if neither source or cell inserts resulted # in conflicts: expected_partial = [local[0], source + ["local\n", "remote\n"], remote[1]] expected_conflicts = [] elif 1: # This is how it would look if source inserts but not cell inserts # resulted in conflicts: expected_partial = [local[0], source, remote[1]] expected_conflicts = [{ "common_path": ("cells", 0, "source"), "local_diff": [op_addrange(len(source), ["local\n"])], "remote_diff": [op_addrange(len(source), ["remote\n"])] }] else: # In current behaviour: # - base cell 0 is aligned correctly (this is the notebook diff heuristics) # - conflicting edits of base cell 0 detected # - local insert before cell 0 is treated as part of conflict on base cell 0 # - remote insert after cell 0 is not treated as part of conflict # FIXME: This may not be the exact behaviour we want: # - For source, we might want both inserts to be part of the conflict. # (if so, fix in generic merge and chunk collection) # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?) # - Figure out the best behaviour and make it happen! expected_partial = [source, remote[1]] expected_conflicts = [{ "common_path": ("cells",), "local_diff": [ op_addrange(0, [nbformat.v4.new_code_cell( source=["new local cell"])]), op_patch(0, [op_patch("source", [ op_addrange(len("".join(source)), "local\n")])]), ], "remote_diff": [op_patch(0, [op_patch('source', [ op_addrange(len("".join(source)), "remote\n")])])] }] _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
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_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_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_merge_inserts_within_deleted_range(): # Multiple inserts within a deleted range base = """ def f(x): return x**2 def g(x): return x + 2 """ # Insert foo and bar local = """ def foo(y): return y / 3 def f(x): return x**2 def bar(y): return y - 3 def g(x): return x + 2 """ remote = "" # Delete all if 0: # This is quite optimistic and would require employing aggressive # attempts at automatic resolution beyond what git and meld do: expected_partial = """ def foo(y): return y / 3 def bar(y): return y - 3 """ else: expected_partial = base expected_conflicts = [{ "common_path": ("cells",), "local_diff": [op_patch("cells", [op_addrange(0, [ nbformat.v4.new_code_cell(local)]), op_removerange(0, 1)])], "remote_diff": [op_patch("cells", [op_addrange(0, [ nbformat.v4.new_code_cell(remote)]), op_removerange(0, 1)])] }] _check_sources(base, local, remote, expected_partial, expected_conflicts) # Keep it failing assert False
def test_merge_insert_cells_around_conflicting_cell(): # Modifying an original cell and inserting a new cell on both sides source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["new local cell"], source + ["local"]] base = [source] remote = [source + ["remote"], ["new remote cell"]] if 0: # This is how it would look if neither source or cell inserts resulted in conflicts: expected_partial = [["new local cell"], source + ["local", "remote"], ["new remote cell"]] expected_lco = [] expected_rco = [] elif 0: # This is how it would look if source inserts but not cell inserts resulted in conflicts: expected_partial = [local[0], source, remote[1]] expected_lco = _patch_cell_source( 1, [op_addrange(len("\n".join(source)), "\nlocal")]) expected_rco = _patch_cell_source( 1, [op_addrange(len("\n".join(source)), "\nremote")]) else: # In current behaviour: # - base cell 0 is aligned correctly (this is the notebook diff heuristics) # - conflicting edits of base cell 0 detected # - local insert before cell 0 is treated as part of conflict on base cell 0 # - remote insert after cell 0 is not treated as part of conflict # FIXME: This may not be the exact behaviour we want: # - For source, we might want both inserts to be part of the conflict. # (if so, fix in generic merge and chunk collection) # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?) # - Figure out the best behaviour and make it happen! expected_partial = [source, remote[1]] expected_lco = [ op_patch("cells", [ op_addrange( 0, [nbformat.v4.new_code_cell(source=["new local cell"])]), op_patch(0, [ op_patch("source", [op_addrange(len("\n".join(source)), "\nlocal")]) ]), ]) ] expected_rco = _patch_cell_source( 0, [op_addrange(len("\n".join(source)), "\nremote")]) _check(base, local, remote, expected_partial, expected_lco, expected_rco)
def pick_merge_decision(base, dec): if dec.action is None or dec.action == "base": di = None elif dec.action == "local" or dec.action == "either": di = dec.local_diff elif dec.action == "remote": di = dec.remote_diff elif dec.action == "custom": di = dec.custom_diff else: raise ValueError("Unknown action {}".format(dec.action)) if di is not None: # Parse common path keys = [k for k in dec.common_path.split("/") if k != ""] sub = base for k in keys: if isinstance(sub, list): k = int(k) sub = sub[k] # "/cells" -> sub = base[cells], sub is a list # patch # Add patch entries base_diff = di for k in reversed(keys): if is_int(k): k = int(k) base_diff = op_patch(k, base_diff) # Apply patch base = patch(base, base_diff) return base
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: expected_partial = [[ "<<<<<<< local\n", "local\n", #"||||||| base\n", #"base\n", "=======\n", "remote\n", ">>>>>>> remote\n", "some other\n", "lines\n", "to align\n", ]] # Current case: _expected_partial = [[ "<<<<<<< local\n", "local\nsome other\nlines\nto align\n", "||||||| base\n", "base\nsome other\nlines\nto align\n", "=======\n", "remote\nsome other\nlines\nto align\n", ">>>>>>> remote"]] expected_conflicts = [{ "common_path": ("cells", 0), "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)], )], }] 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_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_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_merge_insert_cells_around_conflicting_cell(): # Modifying an original cell and inserting a new cell on both sides source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["new local cell"], source + ["local"]] base = [source] remote = [source + ["remote"], ["new remote cell"]] if 0: # This is how it would look if neither source or cell inserts resulted in conflicts: expected_partial = [["new local cell"], source + ["local", "remote"], ["new remote cell"]] expected_lco = [] expected_rco = [] elif 0: # This is how it would look if source inserts but not cell inserts resulted in conflicts: expected_partial = [local[0], source, remote[1]] expected_lco = _patch_cell_source(1, [op_addrange(len("\n".join(source)), "\nlocal")]) expected_rco = _patch_cell_source(1, [op_addrange(len("\n".join(source)), "\nremote")]) else: # In current behaviour: # - base cell 0 is aligned correctly (this is the notebook diff heuristics) # - conflicting edits of base cell 0 detected # - local insert before cell 0 is treated as part of conflict on base cell 0 # - remote insert after cell 0 is not treated as part of conflict # FIXME: This may not be the exact behaviour we want: # - For source, we might want both inserts to be part of the conflict. # (if so, fix in generic merge and chunk collection) # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?) # - Figure out the best behaviour and make it happen! expected_partial = [source, remote[1]] expected_lco = [op_patch("cells", [ op_addrange(0, [nbformat.v4.new_code_cell(source=["new local cell"])]), op_patch(0, [op_patch("source", [op_addrange(len("\n".join(source)), "\nlocal")] )]), ])] expected_rco = _patch_cell_source(0, [op_addrange(len("\n".join(source)), "\nremote")]) _check(base, local, remote, expected_partial, expected_lco, expected_rco)
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_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_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_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_simple_cell_sources(): # A very basic test: Just checking changes to a single cell source, # No change local = [["same"]] base = [["same"]] remote = [["same"]] expected_partial = [["same"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # One sided change local = [["same"]] base = [["same"]] remote = [["different"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # One sided change local = [["different"]] base = [["same"]] remote = [["same"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Same change on both sides local = [["different"]] base = [["same"]] remote = [["different"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Conflicting cell inserts at same location as removing old cell local = [["local"]] base = [["base"]] remote = [["remote"]] expected_partial = [["base"]] expected_lco = [ op_patch("cells", [ op_addrange( 0, [nbformat.v4.new_code_cell(source) for source in local]), op_removerange(0, 1) ]) ] expected_rco = [ op_patch("cells", [ op_addrange( 0, [nbformat.v4.new_code_cell(source) for source in remote]), op_removerange(0, 1) ]) ] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Cell inserts at same location but no other modifications: should this be accepted? local = [["base"], ["local"]] base = [["base"]] remote = [["base"], ["remote"]] if 0: # Treat as conflict expected_partial = [["base"]] expected_lco = [ op_patch("cells", [ op_addrange( 1, [nbformat.v4.new_code_cell(source) for source in local]), ]) ] expected_rco = [ op_patch("cells", [ op_addrange( 1, [nbformat.v4.new_code_cell(source) for source in remote]), ]) ] else: # Treat as non-conflict (insert both) expected_partial = [["base"], ["local"], ["remote"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco)
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 _patch_cell_source(cell_index, source_diff): "Convenience function to create the diff that patches only the source of a specific cell." return [op_patch("cells", [op_patch(cell_index, [op_patch("source", source_diff)])])]
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_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_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_simple_cell_sources(): # A very basic test: Just checking changes to a single cell source, # No change local = [["same"]] base = [["same"]] remote = [["same"]] expected_partial = [["same"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # One sided change local = [["same"]] base = [["same"]] remote = [["different"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # One sided change local = [["different"]] base = [["same"]] remote = [["same"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Same change on both sides local = [["different"]] base = [["same"]] remote = [["different"]] expected_partial = [["different"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Conflicting cell inserts at same location as removing old cell local = [["local"]] base = [["base"]] remote = [["remote"]] expected_partial = [["base"]] expected_lco = [op_patch("cells", [ op_addrange(0, [nbformat.v4.new_code_cell(source) for source in local]), op_removerange(0, 1)])] expected_rco = [op_patch("cells", [ op_addrange(0, [nbformat.v4.new_code_cell(source) for source in remote]), op_removerange(0, 1)])] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Cell inserts at same location but no other modifications: should this be accepted? local = [["base"], ["local"]] base = [["base"]] remote = [["base"], ["remote"]] if 0: # Treat as conflict expected_partial = [["base"]] expected_lco = [op_patch("cells", [ op_addrange(1, [nbformat.v4.new_code_cell(source) for source in local]), ])] expected_rco = [op_patch("cells", [ op_addrange(1, [nbformat.v4.new_code_cell(source) for source in remote]), ])] else: # Treat as non-conflict (insert both) expected_partial = [["base"], ["local"], ["remote"]] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco)
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_inline_merge_notebook_metadata_reproduce_bug(reset_log): md_in = { 1: { "unconflicted": { "list_deleteitem": [7, "deleteme", 3, "notme", 5, "deletemetoo"], }, "conflicted": { "dict_delete_replace": { "k": "v" }, } }, 2: { "unconflicted": { "list_deleteitem": [7, 3, "notme", 5, "deletemetoo"], }, "conflicted": {} }, 3: { "unconflicted": { "list_deleteitem": [7, "deleteme", 3, "notme", 5], }, "conflicted": { "dict_delete_replace": { "k": "x" }, } } } def join_dicts(dicta, dictb): d = {} d.update(dicta) d.update(dictb) return d shared_unconflicted = { "list_deleteitem": [7, 3, "notme", 5], } shared_conflicted = { "dict_delete_replace": { "k": "v" }, } md_out = { (1, 2, 3): { "unconflicted": shared_unconflicted, "conflicted": shared_conflicted }, } # Fill in expected conflict records for triplet in sorted(md_out.keys()): i, j, k = triplet local_diff = diff(md_in[i]["conflicted"], md_in[j]["conflicted"]) remote_diff = diff(md_in[i]["conflicted"], md_in[k]["conflicted"]) # This may not be a necessary test, just checking my expectations assert local_diff == sorted(local_diff, key=lambda x: x.key) assert remote_diff == sorted(remote_diff, key=lambda x: x.key) c = { # These are patches on the /metadata dict "local_diff": [op_patch("conflicted", local_diff)], "remote_diff": [op_patch("conflicted", remote_diff)], } md_out[triplet]["nbdime-conflicts"] = c # Check handcrafted merge results triplet = (1, 2, 3) if 1: i, j, k = triplet base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) expected = new_notebook(metadata=md_out[triplet]) merged, decisions = merge_notebooks(base, local, remote) if "nbdime-conflicts" in merged["metadata"]: assert any([d.conflict for d in decisions]) else: assert not any([d.conflict for d in decisions]) assert expected == merged
def test_inline_merge_notebook_metadata(reset_log): """Merging a wide range of different value types and conflict types in the root /metadata dicts. The goal is to exercise a decent part of the generic diff and merge functionality. """ untouched = { "string": "untouched string", "integer": 123, "float": 16.0, "list": ["hello", "world"], "dict": {"first": "Hello", "second": "World"}, } md_in = { 1: { "untouched": untouched, "unconflicted": { "int_deleteme": 7, "string_deleteme": "deleteme", "list_deleteme": [7, "deleteme"], "dict_deleteme": {"deleteme": "now", "removeme": True}, "list_deleteitem": [7, "deleteme", 3, "notme", 5, "deletemetoo"], "string": "string v1", "integer": 456, "float": 32.0, "list": ["hello", "universe"], "dict": {"first": "Hello", "second": "World", "third": "!"}, }, "conflicted": { "int_delete_replace": 3, "string_delete_replace": "string that will be deleted and modified", "list_delete_replace": [1], "dict_delete_replace": {"k":"v"}, # "string": "string v1", # "integer": 456, # "float": 32.0, # "list": ["hello", "universe"], # "dict": {"first": "Hello", "second": "World"}, } }, 2: { "untouched": untouched, "unconflicted": { "dict_deleteme": {"deleteme": "now", "removeme": True}, "list_deleteitem": [7, 3, "notme", 5, "deletemetoo"], "string": "string v1 equal addition", "integer": 123, # equal change "float": 16.0, # equal change # Equal delete at beginning and insert of two values at end: "list": ["universe", "new items", "same\non\nboth\nsides"], # cases covered: twosided equal value change, onesided delete, onesided replace, onesided insert, twosided insert of same value "dict": {"first": "changed", "second": "World", "third": "!", "newkey": "newvalue", "otherkey": "othervalue"}, }, "conflicted": { "int_delete_replace": 5, "list_delete_replace": [2], # "string": "another text", #"integer": 456, # "float": 16.0, # "list": ["hello", "world"], # "dict": {"new": "value", "first": "Hello"}, #"second": "World"}, # "added_string": "another text", # "added_integer": 9, # "added_float": 16.0, # "added_list": ["another", "multiverse"], # "added_dict": {"1st": "hey", "2nd": "there"}, } }, 3: { "untouched": untouched, "unconflicted": { "list_deleteme": [7, "deleteme"], "list_deleteitem": [7, "deleteme", 3, "notme", 5], "string": "string v1 equal addition", "integer": 123, # equal change "float": 16.0, # equal change # Equal delete at beginning and insert of two values at end: "list": ["universe", "new items", "same\non\nboth\nsides"], "dict": {"first": "changed", "third": ".", "newkey": "newvalue"}, }, "conflicted": { "string_delete_replace": "string that is modified here and deleted in the other version", "dict_delete_replace": {"k":"x","q":"r"}, # "string": "different message", # "integer": 456, # #"float": 16.0, # "list": ["hello", "again", "world"], # "dict": {"new": "but different", "first": "Hello"}, #"second": "World"}, # "added_string": "but not the same string", # #"added_integer": 9, # "added_float": 64.0, # "added_list": ["initial", "values", "another", "multiverse", "trailing", "values"], # "added_dict": {"3rt": "mergeme", "2nd": "conflict"}, } } } def join_dicts(dicta, dictb): d = {} d.update(dicta) d.update(dictb) return d shared_unconflicted = { "list_deleteitem": [7, 3, "notme", 5], "string": "string v1 equal addition", "integer": 123, "float": 16.0, "list": ["universe", "new items", "same\non\nboth\nsides"], "dict": {"first": "changed", "third": ".", "newkey": "newvalue", "otherkey": "othervalue"}, } shared_conflicted = { "int_delete_replace": 3, "string_delete_replace": "string that will be deleted and modified", "list_delete_replace": [1], "dict_delete_replace": {"k":"v"}, # #"string": "string v1", # "string": "another textdifferent message", # "float": 32.0, # "list": ["hello", "universe"], # "dict": {"first": "Hello", "second": "World"}, # # FIXME } md_out = { (1,2,3): { "untouched": untouched, "unconflicted": join_dicts(shared_unconflicted, { # ... }), "conflicted": join_dicts(shared_conflicted, { # ... }), }, (1,3,2): { "untouched": untouched, "unconflicted": join_dicts(shared_unconflicted, { # ... }), "conflicted": join_dicts(shared_conflicted, { # ... }), }, } # Fill in expected conflict records for triplet in sorted(md_out.keys()): i, j, k = triplet local_diff = diff(md_in[i]["conflicted"], md_in[j]["conflicted"]) remote_diff = diff(md_in[i]["conflicted"], md_in[k]["conflicted"]) # This may not be a necessary test, just checking my expectations assert local_diff == sorted(local_diff, key=lambda x: x.key) assert remote_diff == sorted(remote_diff, key=lambda x: x.key) c = { # These are patches on the /metadata dict "local_diff": [op_patch("conflicted", local_diff)], "remote_diff": [op_patch("conflicted", remote_diff)], } md_out[triplet]["nbdime-conflicts"] = c # Fill in the trivial merge results for i in (1, 2, 3): for j in (1, 2, 3): for k in (i, j): # For any combination i,j,i or i,j,j the # result should be j with no conflicts md_out[(i,j,k)] = md_in[j] tested = set() # Check the trivial merge results for i in (1, 2, 3): for j in (1, 2, 3): for k in (i, j): triplet = (i, j, k) tested.add(triplet) base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) # For any combination i,j,i or i,j,j the result should be j expected = new_notebook(metadata=md_in[j]) merged, decisions = merge_notebooks(base, local, remote) assert "nbdime-conflicts" not in merged["metadata"] assert not any([d.conflict for d in decisions]) assert expected == merged # Check handcrafted merge results for triplet in sorted(md_out.keys()): i, j, k = triplet tested.add(triplet) base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) expected = new_notebook(metadata=md_out[triplet]) merged, decisions = merge_notebooks(base, local, remote) if "nbdime-conflicts" in merged["metadata"]: assert any([d.conflict for d in decisions]) else: assert not any([d.conflict for d in decisions]) assert expected == merged # At least try to run merge without crashing for permutations # of md_in that we haven't constructed expected results for for i in (1, 2, 3): for j in (1, 2, 3): for k in (1, 2, 3): triplet = (i, j, k) if triplet not in tested: base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) merged, decisions = merge_notebooks(base, local, remote)
def test_inline_merge_notebook_metadata(reset_log): """Merging a wide range of different value types and conflict types in the root /metadata dicts. The goal is to exercise a decent part of the generic diff and merge functionality. """ untouched = { "string": "untouched string", "integer": 123, "float": 16.0, "list": ["hello", "world"], "dict": { "first": "Hello", "second": "World" }, } md_in = { 1: { "untouched": untouched, "unconflicted": { "int_deleteme": 7, "string_deleteme": "deleteme", "list_deleteme": [7, "deleteme"], "dict_deleteme": { "deleteme": "now", "removeme": True }, "list_deleteitem": [7, "deleteme", 3, "notme", 5, "deletemetoo"], "string": "string v1", "integer": 456, "float": 32.0, "list": ["hello", "universe"], "dict": { "first": "Hello", "second": "World", "third": "!" }, }, "conflicted": { "int_delete_replace": 3, "string_delete_replace": "string that will be deleted and modified", "list_delete_replace": [1], "dict_delete_replace": { "k": "v" }, # "string": "string v1", # "integer": 456, # "float": 32.0, # "list": ["hello", "universe"], # "dict": {"first": "Hello", "second": "World"}, } }, 2: { "untouched": untouched, "unconflicted": { "dict_deleteme": { "deleteme": "now", "removeme": True }, "list_deleteitem": [7, 3, "notme", 5, "deletemetoo"], "string": "string v1 equal addition", "integer": 123, # equal change "float": 16.0, # equal change # Equal delete at beginning and insert of two values at end: "list": ["universe", "new items", "same\non\nboth\nsides"], # cases covered: twosided equal value change, onesided delete, onesided replace, onesided insert, twosided insert of same value "dict": { "first": "changed", "second": "World", "third": "!", "newkey": "newvalue", "otherkey": "othervalue" }, }, "conflicted": { "int_delete_replace": 5, "list_delete_replace": [2], # "string": "another text", #"integer": 456, # "float": 16.0, # "list": ["hello", "world"], # "dict": {"new": "value", "first": "Hello"}, #"second": "World"}, # "added_string": "another text", # "added_integer": 9, # "added_float": 16.0, # "added_list": ["another", "multiverse"], # "added_dict": {"1st": "hey", "2nd": "there"}, } }, 3: { "untouched": untouched, "unconflicted": { "list_deleteme": [7, "deleteme"], "list_deleteitem": [7, "deleteme", 3, "notme", 5], "string": "string v1 equal addition", "integer": 123, # equal change "float": 16.0, # equal change # Equal delete at beginning and insert of two values at end: "list": ["universe", "new items", "same\non\nboth\nsides"], "dict": { "first": "changed", "third": ".", "newkey": "newvalue" }, }, "conflicted": { "string_delete_replace": "string that is modified here and deleted in the other version", "dict_delete_replace": { "k": "x", "q": "r" }, # "string": "different message", # "integer": 456, # #"float": 16.0, # "list": ["hello", "again", "world"], # "dict": {"new": "but different", "first": "Hello"}, #"second": "World"}, # "added_string": "but not the same string", # #"added_integer": 9, # "added_float": 64.0, # "added_list": ["initial", "values", "another", "multiverse", "trailing", "values"], # "added_dict": {"3rt": "mergeme", "2nd": "conflict"}, } } } def join_dicts(dicta, dictb): d = {} d.update(dicta) d.update(dictb) return d shared_unconflicted = { "list_deleteitem": [7, 3, "notme", 5], "string": "string v1 equal addition", "integer": 123, "float": 16.0, "list": ["universe", "new items", "same\non\nboth\nsides"], "dict": { "first": "changed", "third": ".", "newkey": "newvalue", "otherkey": "othervalue" }, } shared_conflicted = { "int_delete_replace": 3, "string_delete_replace": "string that will be deleted and modified", "list_delete_replace": [1], "dict_delete_replace": { "k": "v" }, # #"string": "string v1", # "string": "another textdifferent message", # "float": 32.0, # "list": ["hello", "universe"], # "dict": {"first": "Hello", "second": "World"}, # # FIXME } md_out = { (1, 2, 3): { "untouched": untouched, "unconflicted": join_dicts( shared_unconflicted, { # ... }), "conflicted": join_dicts( shared_conflicted, { # ... }), }, (1, 3, 2): { "untouched": untouched, "unconflicted": join_dicts( shared_unconflicted, { # ... }), "conflicted": join_dicts( shared_conflicted, { # ... }), }, } # Fill in expected conflict records for triplet in sorted(md_out.keys()): i, j, k = triplet local_diff = diff(md_in[i]["conflicted"], md_in[j]["conflicted"]) remote_diff = diff(md_in[i]["conflicted"], md_in[k]["conflicted"]) # This may not be a necessary test, just checking my expectations assert local_diff == sorted(local_diff, key=lambda x: x.key) assert remote_diff == sorted(remote_diff, key=lambda x: x.key) c = { # These are patches on the /metadata dict "local_diff": [op_patch("conflicted", local_diff)], "remote_diff": [op_patch("conflicted", remote_diff)], } md_out[triplet]["nbdime-conflicts"] = c # Fill in the trivial merge results for i in (1, 2, 3): for j in (1, 2, 3): for k in (i, j): # For any combination i,j,i or i,j,j the # result should be j with no conflicts md_out[(i, j, k)] = md_in[j] tested = set() # Check the trivial merge results for i in (1, 2, 3): for j in (1, 2, 3): for k in (i, j): triplet = (i, j, k) tested.add(triplet) base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) # For any combination i,j,i or i,j,j the result should be j expected = new_notebook(metadata=md_in[j]) merged, decisions = merge_notebooks(base, local, remote) assert "nbdime-conflicts" not in merged["metadata"] assert not any([d.conflict for d in decisions]) assert expected == merged # Check handcrafted merge results for triplet in sorted(md_out.keys()): i, j, k = triplet tested.add(triplet) base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) expected = new_notebook(metadata=md_out[triplet]) merged, decisions = merge_notebooks(base, local, remote) if "nbdime-conflicts" in merged["metadata"]: assert any([d.conflict for d in decisions]) else: assert not any([d.conflict for d in decisions]) assert expected == merged # At least try to run merge without crashing for permutations # of md_in that we haven't constructed expected results for for i in (1, 2, 3): for j in (1, 2, 3): for k in (1, 2, 3): triplet = (i, j, k) if triplet not in tested: base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) merged, decisions = merge_notebooks(base, local, remote)
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_inline_merge_notebook_metadata_reproduce_bug(reset_log): md_in = { 1: { "unconflicted": { "list_deleteitem": [7, "deleteme", 3, "notme", 5, "deletemetoo"], }, "conflicted": { "dict_delete_replace": {"k":"v"}, } }, 2: { "unconflicted": { "list_deleteitem": [7, 3, "notme", 5, "deletemetoo"], }, "conflicted": { } }, 3: { "unconflicted": { "list_deleteitem": [7, "deleteme", 3, "notme", 5], }, "conflicted": { "dict_delete_replace": {"k":"x"}, } } } def join_dicts(dicta, dictb): d = {} d.update(dicta) d.update(dictb) return d shared_unconflicted = { "list_deleteitem": [7, 3, "notme", 5], } shared_conflicted = { "dict_delete_replace": {"k":"v"}, } md_out = { (1,2,3): { "unconflicted": shared_unconflicted, "conflicted": shared_conflicted }, } # Fill in expected conflict records for triplet in sorted(md_out.keys()): i, j, k = triplet local_diff = diff(md_in[i]["conflicted"], md_in[j]["conflicted"]) remote_diff = diff(md_in[i]["conflicted"], md_in[k]["conflicted"]) # This may not be a necessary test, just checking my expectations assert local_diff == sorted(local_diff, key=lambda x: x.key) assert remote_diff == sorted(remote_diff, key=lambda x: x.key) c = { # These are patches on the /metadata dict "local_diff": [op_patch("conflicted", local_diff)], "remote_diff": [op_patch("conflicted", remote_diff)], } md_out[triplet]["nbdime-conflicts"] = c # Check handcrafted merge results triplet = (1,2,3) if 1: i, j, k = triplet base = new_notebook(metadata=md_in[i]) local = new_notebook(metadata=md_in[j]) remote = new_notebook(metadata=md_in[k]) expected = new_notebook(metadata=md_out[triplet]) merged, decisions = merge_notebooks(base, local, remote) if "nbdime-conflicts" in merged["metadata"]: assert any([d.conflict for d in decisions]) else: assert not any([d.conflict for d in decisions]) assert expected == merged
def _patch_cell_source(cell_index, source_diff): "Convenience function to create the diff that patches only the source of a specific cell." return [ op_patch("cells", [op_patch(cell_index, [op_patch("source", source_diff)])]) ]
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_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_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) ]), ]