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_deep_merge_lists_delete_no_conflict(): # local removes an entry b = [[1, 3, 5], [2, 4, 6]] for i in range(len(b)): for j in range(len(b[i])): l = copy.deepcopy(b) r = copy.deepcopy(b) l[i].pop(j) m, lc, rc = merge(b, l, r) assert m == l assert lc == [] assert rc == [] # remote removes an entry b = [[1, 3, 5], [2, 4, 6]] for i in range(len(b)): for j in range(len(b[i])): l = copy.deepcopy(b) r = copy.deepcopy(b) r[i].pop(j) m, lc, rc = merge(b, l, r) assert m == r assert lc == [] assert rc == [] # both remove the same entry and one each b = [[1, 3, 5], [2, 4, 6]] l = [[1, 5], [2, 4]] # deletes 3 and 6 r = [[1, 5], [4, 6]] # deletes 3 and 2 m, lc, rc = merge(b, l, r) #assert m == [[1, 5], [2, 4], [1, 5], [4, 6]] # This was expected behaviour before: clear b, add l, add r #assert m == [[1, 5], [4]] # 2,3,6 should be gone. TODO: This is the naively ideal thought-reading behaviour. Possible? assert m == b # conflicts lead to original kept in m assert lc == [op_addrange(0, l), op_removerange(0, 2)] assert rc == [op_addrange(0, r), op_removerange(0, 2)]
def test_merge_simple_cell_source_conflicting_insert(): # Cell inserts at same location but no other modifications: # should this be accepted? local = [["base"], ["local"]] base = [["base"]] remote = [["base"], ["remote"]] if 1: # Treat as conflict expected_partial = [["base"]] expected_conflicts = [{ "common_path": ("cells",), "local_diff": [op_addrange( 1, [nbformat.v4.new_code_cell(local[1][0])]), ], "remote_diff": [op_addrange( 1, [nbformat.v4.new_code_cell(remote[1][0])]), ] }] else: # Treat as non-conflict (insert both) expected_partial = [["base"], ["local"], ["remote"]] expected_conflicts = [] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
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_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_deep_merge_lists_delete_no_conflict(): # local removes an entry b = [[1, 3, 5], [2, 4, 6]] for i in range(len(b)): for j in range(len(b[i])): l = copy.deepcopy(b) r = copy.deepcopy(b) l[i].pop(j) decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == l assert not any([d.conflict for d in decisions]) # remote removes an entry b = [[1, 3, 5], [2, 4, 6]] for i in range(len(b)): for j in range(len(b[i])): l = copy.deepcopy(b) r = copy.deepcopy(b) r[i].pop(j) decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == r assert not any([d.conflict for d in decisions]) # both remove the same entry and one each b = [[1, 3, 5], [2, 4, 6]] l = [[1, 5], [2, 4]] # deletes 3 and 6 r = [[1, 5], [4, 6]] # deletes 3 and 2 decisions = decide_merge(b, l, r) m = apply_decisions(b, decisions) #assert m == [[1, 5], [2, 4], [1, 5], [4, 6]] # This was expected behaviour before: clear b, add l, add r #assert m == [[1, 5], [4]] # 2,3,6 should be gone. TODO: This is the naively ideal thought-reading behaviour. Possible? assert m == b # conflicts lead to original kept in m assert decisions[0].conflict assert decisions[0].local_diff == [op_addrange(0, l), op_removerange(0, 2)] assert decisions[0].remote_diff == [op_addrange(0, r), op_removerange(0, 2)]
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_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_cell_sources_neighbouring_inserts(): base = sources_to_notebook([[ "def f(x):", " return x**2", ], [ "def g(y):", " return y + 2", ], ]) local = sources_to_notebook([[ "def f(x):", " return x**2", ], [ "print(f(3))", ], [ "def g(y):", " return y + 2", ], ]) remote = sources_to_notebook([[ "def f(x):", " return x**2", ], [ "print(f(7))", ], [ "def g(y):", " return y + 2", ], ]) if 1: expected_partial = base expected_conflicts = [{ "common_path": ("cells",), "local_diff": [op_addrange(1, [local.cells[1]])], "remote_diff": [op_addrange(1, [remote.cells[1]])] }] else: # Strategy local_then_remote: expected_partial = sources_to_notebook([[ "def f(x):", " return x**2", ], [ "print(f(3))", ], [ "print(f(7))", ], [ "def g(y):", " return y + 2", ], ]) expected_conflicts = [] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" _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])) ], }] 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_conflicts_get_diff_indices_shifted(): # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source+["local"], ["different"]] base = [["same"], source+["base"], ["same"]] remote = [["different"], source+["remote"], ["same"]] expected_partial = [["different"], source + ["local", "remote"], ["different"]] expected_conflicts = [{ "common_path": (), "local_diff": [ op_removerange(1, 1), op_addrange(1, ["left"]), ], "remote_diff": [ op_removerange(1, 1), op_addrange(1, ["right"]), ] }] _check_sources(base, local, remote, expected_partial, expected_conflicts) # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source + ["long line with minor change L"], ["different"]] base = [["same"], source + ["long line with minor change"], ["same"]] remote = [["different"], source + ["long line with minor change R"], ["same"]] expected_partial = [["different"], source + ["long line with minor change"], ["different"]] expected_conflicts = [{ "common_path": (), "local_diff": [ op_removerange(1, 1), op_addrange(1, ["left"]), ], "remote_diff": [ op_removerange(1, 1), op_addrange(1, ["right"]), ] }] _check_sources(base, local, remote, expected_partial, expected_conflicts)
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_deep_merge_twosided_inserts_no_conflict(): # local and remote adds an entry each in a new sublist b = [[1]] l = [[1], [2], [3]] r = [[1], [2], [4]] assert diff(b, l) == [op_addrange(1, [[2], [3]])] assert diff(b, r) == [op_addrange(1, [[2], [4]])] m, lc, rc = merge(b, l, r) # No identification of equal inserted list [2] expected from current algorithm assert m == [[1], [2], [3], [2], [4]] assert lc == [] assert rc == []
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_conflicts_get_diff_indices_shifted(): # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source + ["local"], ["different"]] base = [["same"], source + ["base"], ["same"]] remote = [["different"], source + ["remote"], ["same"]] expected_partial = [["different"], source + ["local", "remote"], ["different"]] expected_lco = [ op_removerange(1, 1), op_addrange(1, ["left"]), ] expected_rco = [ op_removerange(1, 1), op_addrange(1, ["right"]), ] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source + ["long line with minor change L"], ["different"]] base = [["same"], source + ["long line with minor change"], ["same"]] remote = [["different"], source + ["long line with minor change R"], ["same"]] expected_partial = [["different"], source + ["long line with minor change"], ["different"]] expected_lco = [ op_removerange(1, 1), op_addrange(1, ["left"]), # todo ] expected_rco = [ op_removerange(1, 1), op_addrange(1, ["right"]), # todo ] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco)
def test_merge_multiline_cell_source_conflict(): # Modifying cell on both sides interpreted as editing the original cell # (this is where heuristics kick in: when is a cell modified and when is it replaced?) source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [source + ["local"]] base = [source] remote = [source + ["remote"]] expected_partial = base expected_lco = _patch_cell_source(0, [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_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_deep_merge_twosided_inserts_conflicted2(): # local and remote adds an entry each in a new sublist b = [[1]] l = [[1], [2], [3]] r = [[1], [2], [4]] assert diff(b, l) == [op_addrange(1, [[2], [3]])] assert diff(b, r) == [op_addrange(1, [[2], [4]])] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [[1], [2]] assert len(decisions) == 2 assert_either_decision(decisions[0], [op_addrange(1, [[2]])]) d = decisions[1] assert d.conflict assert d.common_path == () assert d.local_diff == [op_addrange(1, l[2:])] assert d.remote_diff == [op_addrange(1, r[2:])]
def test_merge_multiline_cell_source_conflict(): # Modifying cell on both sides interpreted as editing the original cell # (this is where heuristics kick in: when is a cell modified and when is it replaced?) source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [source + ["local"]] base = [source] remote = [source + ["remote"]] expected_partial = base expected_lco = _patch_cell_source(0, [op_addrange(len(source), ["local"])]) expected_rco = _patch_cell_source(0, [op_addrange(len(source), ["remote"])]) _check(base, local, remote, expected_partial, expected_lco, expected_rco)
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_merge_conflicts_get_diff_indices_shifted(): # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source+["local"], ["different"]] base = [["same"], source+["base"], ["same"]] remote = [["different"], source+["remote"], ["same"]] expected_partial = [["different"], source+["local", "remote"], ["different"]] expected_lco = [ op_removerange(1, 1), op_addrange(1, ["left"]), ] expected_rco = [ op_removerange(1, 1), op_addrange(1, ["right"]), ] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco) # Trying to induce conflicts with shifting of diff indices source = [ "def foo(x, y):", " z = x * y", " return z", ] local = [["same"], source+["long line with minor change L"], ["different"]] base = [["same"], source+["long line with minor change"], ["same"]] remote = [["different"], source+["long line with minor change R"], ["same"]] expected_partial = [["different"], source+["long line with minor change"], ["different"]] expected_lco = [ op_removerange(1, 1), op_addrange(1, ["left"]), # todo ] expected_rco = [ op_removerange(1, 1), op_addrange(1, ["right"]), # todo ] expected_lco = [] expected_rco = [] _check(base, local, remote, expected_partial, expected_lco, expected_rco)
def test_merge_simple_cell_source_conflicting_edit_aligned(): # Conflicting edit in first line of single 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"]] expected_partial = [["base\n", "some other\n", "lines\n", "to align\n"]] 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)] }] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
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_merge_simple_cell_source_conflicting_edit_aligned(): # 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"]] expected_partial = [["base\n", "some other\n", "lines\n", "to align\n"]] 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)] }] merge_args = copy.deepcopy(args) merge_args.merge_strategy = "mergetool" _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
def test_merge_multiline_cell_source_conflict(): # Modifying cell on both sides interpreted as editing the original cell # (this is where heuristics kick in: when is a cell modified and when is # it replaced?) source = [ "def foo(x, y):\n", " z = x * y\n", " return z\n", ] local = [source + ["local\n"] + [""]] base = [source + [""]] remote = [source + ["remote\n"] + [""]] le = op_addrange(3, "local\n") re = op_addrange(3, "remote\n") expected_partial = base expected_conflicts = [{ "common_path": ("cells", "0", "source"), "local_diff": [le], "remote_diff": [re] }] _check_sources(base, local, remote, expected_partial, expected_conflicts)
def test_deep_merge_lists_insert_conflicted(): # Some notes explaining the below expected values... while this works: assert diff([1], [1, 2]) == [op_addrange(1, [2])] # This does not happen: #assert diff([[1]], [[1, 2]]) == [op_patch(0, [op_addrange(1, [2])])] # Instead we get this: assert diff([[1]], [[1, 2]]) == [op_addrange(0, [[1, 2]]), op_removerange(0, 1)] # To get the "patch inner list" version instead of the "remove inner list + add new inner list" version, # the diff algorithm would need to identify that the inner list [1] is similar to [1,2], # e.g. through heuristics. In the case [1] vs [1,2] the answer will probably be "not similar enough" even # with better heuristics than we have today, i.e. we can never be quite certain what the "right choice" is. # *** Because of this uncertainty, insertions at the same location are suspect and must be treated as conflicts! *** # local and remote adds an entry each to inner list # (documents failure to identify inner list patching opportunity) b = [[1]] l = [[1, 2]] r = [[1, 3]] decisions = decide_merge(b, l, r) #assert apply_decisions(b, decisions) == [[1, 2], [1, 3]] # This was expected behaviour in old code, obviously not what we want #assert apply_decisions(b, decisions) == [[1, 2, 3]] # This is the behaviour we want from an ideal thought-reading algorithm, unclear if possible #assert apply_decisions(b, decisions) == [[1]] # This is the behaviour we get if reverts to base value assert len(decisions) == 1 d = decisions[0] assert d.common_path == () assert d.local_diff == [op_addrange(0, [[1, 2]]), op_removerange(0, 1)] assert d.remote_diff == [op_addrange(0, [[1, 3]]), op_removerange(0, 1)] # local and remote adds the same entry plus an entry each b = [[1]] l = [[1, 2, 4]] r = [[1, 3, 4]] decisions = decide_merge(b, l, r) # No identification of equal inserted value 4 expected from current algorithm #assert apply_decisions(b, decisions) == [[1, 2, 4, 3, 4]] # TODO: Is this the behaviour we want, merge in inner list? #assert apply_decisions(b, decisions) == [[1, 2, 4], [1, 3, 4]] # This was expected behaviour in previous algorithm #assert lc == [] #assert rc == [] assert apply_decisions(b, decisions) == [[ 1 ]] # This is expected behaviour today, base left for conflict resolution assert len(decisions) == 1 d = decisions[0] assert d.common_path == () assert d.local_diff == [op_addrange(0, [[1, 2, 4]]), op_removerange(0, 1)] assert d.remote_diff == [op_addrange(0, [[1, 3, 4]]), op_removerange(0, 1)]
def test_deep_merge_lists_insert_conflicted(): # Some notes explaining the below expected values... while this works: assert diff([1], [1, 2]) == [op_addrange(1, [2])] # This does not happen: #assert diff([[1]], [[1, 2]]) == [op_patch(0, [op_addrange(1, [2])])] # Instead we get this: assert diff([[1]], [[1, 2]]) == [op_addrange(0, [[1, 2]]), op_removerange(0, 1)] # To get the "patch inner list" version instead of the "remove inner list + add new inner list" version, # the diff algorithm would need to identify that the inner list [1] is similar to [1,2], # e.g. through heuristics. In the case [1] vs [1,2] the answer will probably be "not similar enough" even # with better heuristics than we have today, i.e. we can never be quite certain what the "right choice" is. # *** Because of this uncertainty, insertions at the same location are suspect and must be treated as conflicts! *** # local and remote adds an entry each to inner list # (documents failure to identify inner list patching opportunity) b = [[1]] l = [[1, 2]] r = [[1, 3]] decisions = decide_merge(b, l, r) #assert apply_decisions(b, decisions) == [[1, 2], [1, 3]] # This was expected behaviour in old code, obviously not what we want #assert apply_decisions(b, decisions) == [[1, 2, 3]] # This is the behaviour we want from an ideal thought-reading algorithm, unclear if possible #assert apply_decisions(b, decisions) == [[1]] # This is the behaviour we get if reverts to base value assert len(decisions) == 1 d = decisions[0] assert d.common_path == () assert d.local_diff == [op_addrange(0, [[1, 2]]), op_removerange(0, 1)] assert d.remote_diff == [op_addrange(0, [[1, 3]]), op_removerange(0, 1)] # local and remote adds the same entry plus an entry each b = [[1]] l = [[1, 2, 4]] r = [[1, 3, 4]] decisions = decide_merge(b, l, r) # No identification of equal inserted value 4 expected from current algorithm #assert apply_decisions(b, decisions) == [[1, 2, 4, 3, 4]] # TODO: Is this the behaviour we want, merge in inner list? #assert apply_decisions(b, decisions) == [[1, 2, 4], [1, 3, 4]] # This was expected behaviour in previous algorithm #assert lc == [] #assert rc == [] assert apply_decisions(b, decisions) == [[1]] # This is expected behaviour today, base left for conflict resolution assert len(decisions) == 1 d = decisions[0] assert d.common_path == () assert d.local_diff == [op_addrange(0, [[1, 2, 4]]), op_removerange(0, 1)] assert d.remote_diff == [op_addrange(0, [[1, 3, 4]]), op_removerange(0, 1)]
def test_deep_merge_twosided_inserts_conflicted(): # local and remote adds an entry each in a new sublist b = [] l = [[2], [3]] r = [[2], [4]] assert diff(b, l) == [op_addrange(0, [[2], [3]])] assert diff(b, r) == [op_addrange(0, [[2], [4]])] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [[2]] assert len(decisions) == 2 d = decisions[0] assert not d.conflict assert d.common_path == () assert d.action == 'either' assert d.local_diff == [op_addrange(0, [[2]])] assert d.remote_diff == [op_addrange(0, [[2]])] d = decisions[1] assert d.conflict assert d.common_path == () assert d.local_diff == [op_addrange(0, [[3]])] assert d.remote_diff == [op_addrange(0, [[4]])]
def test_shallow_merge_lists_insert_conflicted(): # local and remote adds an entry each b = [1] l = [1, 2] r = [1, 3] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == b assert len(decisions) == 1 d = decisions[0] assert d.conflict assert d.common_path == () assert d.local_diff == [op_addrange(1, [2])] assert d.remote_diff == [op_addrange(1, [3])] # local and remote adds an equal entry plus a different entry each b = [1, 9] l = [1, 2, 7, 9] r = [1, 3, 7, 9] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1, 7, 9] assert len(decisions) == 2 d = decisions[0] assert d.conflict assert d.common_path == () assert d.local_diff == [op_addrange(1, [2])] assert d.remote_diff == [op_addrange(1, [3])] d = decisions[1] assert d.common_path == () assert_either_decision(d, [op_addrange(1, [7])]) # local and remote adds entries to empty base b = [] l = [1, 2, 4] r = [1, 3, 4] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1, 4] assert len(decisions) == 3 d = decisions[0] assert d.common_path == () assert_either_decision(d, [op_addrange(0, [1])]) d = decisions[1] assert d.conflict assert d.common_path == () assert d.local_diff == [op_addrange(0, [2])] assert d.remote_diff == [op_addrange(0, [3])] d = decisions[2] assert d.common_path == () assert_either_decision(d, [op_addrange(0, [4])]) # local and remote adds different entries interleaved within each base entry b = [2, 5, 8] l = [0, 2, 3, 5, 6, 8, 9] r = [1, 2, 4, 5, 7, 8, 10] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == b assert len(decisions) == 4 assert all([d.conflict for d in decisions]) assert all([d.common_path == () for d in decisions]) assert decisions[0].local_diff == [op_addrange(0, [0])] assert decisions[0].remote_diff == [op_addrange(0, [1])] assert decisions[1].local_diff == [op_addrange(1, [3])] assert decisions[1].remote_diff == [op_addrange(1, [4])] assert decisions[2].local_diff == [op_addrange(2, [6])] assert decisions[2].remote_diff == [op_addrange(2, [7])] assert decisions[3].local_diff == [op_addrange(3, [9])] assert decisions[3].remote_diff == [op_addrange(3, [10])]
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_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)