def test_autoresolve_list_conflicting_insertions_simple(): # local and remote adds an entry each b = [1] l = [1, 2] r = [1, 3] decisions = decide_merge(b, l, r) strategies = {"/*": "use-local"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = {"/*": "use-remote"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = {"/*": "use-base"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == b assert not any(d.conflict for d in resolved) strategies = {"/*": "join"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3] assert not any(d.conflict for d in resolved) strategies = {"/*": "clear-parent"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)
def test_autoresolve_mixed_nested_transients(): # For this test, we need to use a custom predicate to ensure alignment common = {'id': 'This ensures alignment'} predicates = defaultdict(lambda: [operator.__eq__], { '/': [lambda a, b: a['id'] == b['id']], }) # Setup transient difference in base and local, deletion in remote b = [{'a': {'transient': 22}}] l = [{'a': {'transient': 242}}] b[0].update(common) l[0].update(common) r = [] # Make decisions based on diffs with predicates ld = diff(b, l, path="", predicates=predicates) rd = diff(b, r, path="", predicates=predicates) decisions = decide_merge_with_diff(b, l, r, ld, rd) # Assert that generic merge gives conflict assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Without strategy, no progress is made: resolved = autoresolve(b, decisions, Strategies()) assert resolved == decisions # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=[ '/*/a/transient' ]) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved)
def test_autoresolve_list_conflicting_insertions_simple(): # local and remote adds an entry each b = [1] l = [1, 2] r = [1, 3] decisions = decide_merge(b, l, r) strategies = Strategies({"/*": "use-local"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-remote"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-base"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == b assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "union"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3] assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "clear-all"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)
def test_autoresolve_mixed_nested_transients(): # For this test, we need to use a custom predicate to ensure alignment common = {'id': 'This ensures alignment'} predicates = defaultdict(lambda: [operator.__eq__], { '/': [lambda a, b: a['id'] == b['id']], }) # Setup transient difference in base and local, deletion in remote b = [{'a': {'transient': 22}}] l = [{'a': {'transient': 242}}] b[0].update(common) l[0].update(common) r = [] # Make decisions based on diffs with predicates ld = diff(b, l, path="", predicates=predicates) rd = diff(b, r, path="", predicates=predicates) decisions = decide_merge_with_diff(b, l, r, ld, rd) # Assert that generic merge gives conflict assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Without strategy, no progress is made: resolved = autoresolve(b, decisions, Strategies()) assert resolved == decisions # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=['/*/a/transient']) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved)
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_deep_merge_dicts_delete_no_conflict(): # local removes an entry b = {"p": {"b": 1, "a": 3}} l = {"p": {"b": 1}} r = {"p": {"b": 1, "a": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1}} assert not any([d.conflict for d in decisions]) # remote removes an entry b = {"p": {"b": 1, "a": 3}} l = {"p": {"b": 1, "a": 3}} r = {"p": {"b": 1}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1}} assert not any([d.conflict for d in decisions]) # both remove the same entry b = {"p": {"b": 1, "a": 3}} l = {"p": {"b": 1}} r = {"p": {"b": 1}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1}} assert not any([d.conflict for d in decisions]) # both remove the same entry and one each b = {"p": {"b": 1, "a": 3, "c": 5, "d": 7}} l = {"p": {"b": 1, "c": 5}} r = {"p": {"b": 1, "d": 7}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1}} assert not any([d.conflict for d in decisions])
def test_autoresolve_dict_transients(): # Setup transient difference in base and local, deletion in remote b = {'a': {'transient': 22}} l = {'a': {'transient': 242}} r = {} # Make decisions based on diffs with predicates decisions = decide_merge(b, l, r) # Assert that generic merge gives conflict assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Without strategy, no progress is made: resolved = autoresolve(b, decisions, Strategies()) assert resolved == decisions # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=[ '/a/transient' ]) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved)
def test_deep_merge_dicts_insert_no_conflict(): # local adds an entry b = {"p": {"b": 1}} l = {"p": {"b": 1, "l": 2}} r = {"p": {"b": 1}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1, "l": 2}} assert not any([d.conflict for d in decisions]) # remote adds an entry b = {"p": {"b": 1}} l = {"p": {"b": 1}} r = {"p": {"b": 1, "r": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1, "r": 3}} assert not any([d.conflict for d in decisions]) # local and remote adds an entry each b = {"p": {"b": 1}} l = {"p": {"b": 1, "l": 2}} r = {"p": {"b": 1, "r": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"p": {"b": 1, "l": 2, "r": 3}} assert not any([d.conflict for d in decisions]) # local and remote adds the same entry plus an entry each b = {"p": {"b": 1}} l = {"p": {"b": 1, "s": 7, "l": 2}} r = {"p": {"b": 1, "s": 7, "r": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == { "p": {"b": 1, "s": 7, "l": 2, "r": 3}} assert not any([d.conflict for d in decisions])
def test_autoresolve_dict_transients(): # For this test, we need to use a custom predicate to ensure alignment common = {'id': 'This ensures alignment'} # Setup transient difference in base and local, deletion in remote b = {'a': {'transient': 22}} l = {'a': {'transient': 242}} r = {} # Make decisions based on diffs with predicates decisions = decide_merge(b, l, r) # Assert that generic merge gives conflict assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Without strategy, no progress is made: resolved = autoresolve(b, decisions, Strategies()) assert resolved == decisions # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=['/a/transient']) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved)
def test_shallow_merge_lists_insert_no_conflict(): # local adds an entry b = [1] l = b + [2] r = copy.deepcopy(b) decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1, 2] assert not any([d.conflict for d in decisions]) # remote adds an entry b = [1] l = copy.deepcopy(b) r = b + [3] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1, 3] assert not any([d.conflict for d in decisions]) # local and remote adds the same entries interleaved within each base entry b = [1, 3, 5] l = [0, 1, 2, 3, 4, 5] r = copy.deepcopy(l) decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == l assert not any([d.conflict for d in decisions])
def test_shallow_merge_dicts_insert_no_conflict(): # local adds an entry b = {"b": 1} l = {"b": 1, "l": 2} r = {"b": 1} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1, "l": 2} assert not any([d.conflict for d in decisions]) # remote adds an entry b = {"b": 1} l = {"b": 1} r = {"b": 1, "r": 3} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1, "r": 3} assert not any([d.conflict for d in decisions]) # local and remote adds an entry each b = {"b": 1} l = {"b": 1, "l": 2} r = {"b": 1, "r": 3} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1, "l": 2, "r": 3} assert not any([d.conflict for d in decisions]) # local and remote adds an equal entry plus a different entry each b = {"b": 1} l = {"b": 1, "l": 2, "s": 7} r = {"b": 1, "r": 3, "s": 7} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1, "l": 2, "r": 3, "s": 7} assert not any([d.conflict for d in decisions])
def test_deep_merge_onesided_inner_list_insert_no_conflict(): # local adds an entry b = [[1]] l = [[1, 2]] r = [[1]] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [[1, 2]] assert not any([d.conflict for d in decisions]) # remote adds an entry b = [[1]] l = [[1]] r = [[1, 3]] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [[1, 3]] assert not any([d.conflict for d in decisions])
def test_shallow_merge_lists_delete_no_conflict(): # local removes an entry b = [1, 3] l = [1] r = [1, 3] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1] assert not any([d.conflict for d in decisions]) # remote removes an entry b = [1, 3] l = [1, 3] r = [1] decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == [1] assert not any([d.conflict for d in decisions]) # both remove the same entry b = [1, 3, 2, 7] for i in range(len(b)): l = copy.deepcopy(b) r = copy.deepcopy(b) l.pop(i) r.pop(i) decisions = decide_merge(b, l, r) e = copy.deepcopy(b) e.pop(i) assert apply_decisions(b, decisions) == e assert not any([d.conflict for d in decisions]) # both remove the same entry plus one other each b = [1, 3, 2, 7] for i in range(len(b)): for j in range(len(b)): if j == i: continue for k in range(len(b)): if k == i or k == j: continue l = cut(b, i, j) r = cut(b, i, k) e = cut(b, i, j, k) decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == e assert not any([d.conflict for d in decisions])
def test_autoresolve_dict_clear(): """Check strategy "clear" in various cases.""" base2 = {"foo": [1, 2]} local2 = {"foo": [1, 4, 2]} remote2 = {"foo": [1, 3, 2]} decisions = decide_merge(base2, local2, remote2) assert apply_decisions(base2, decisions) == {"foo": [1, 2]} assert decisions[0].local_diff != [] assert decisions[0].remote_diff != [] strategies = {"/foo": "clear-parent"} resolved = autoresolve(base2, decisions, strategies) assert apply_decisions(base2, resolved) == {"foo": []} assert not any([d.conflict for d in resolved]) strategies = {"/foo": "clear"} resolved = autoresolve(base2, decisions, strategies) assert apply_decisions(base2, resolved) == {"foo": [1, None]} assert not any([d.conflict for d in resolved])
def test_autoresolve_dict_clear(): """Check strategy "clear" in various cases.""" base2 = {"foo": [1, 2]} local2 = {"foo": [1, 4, 2]} remote2 = {"foo": [1, 3, 2]} decisions = decide_merge(base2, local2, remote2) assert apply_decisions(base2, decisions) == {"foo": [1, 2]} assert decisions[0].local_diff != [] assert decisions[0].remote_diff != [] strategies = Strategies({"/foo": "clear-all"}) resolved = autoresolve(base2, decisions, strategies) assert apply_decisions(base2, resolved) == {"foo": []} assert not any([d.conflict for d in resolved]) strategies = Strategies({"/foo": "clear"}) resolved = autoresolve(base2, decisions, strategies) assert apply_decisions(base2, resolved) == {"foo": [1, None]} assert not any([d.conflict for d in resolved])
def test_merge_nonconflicting_nested_dicts(): # local and remote each adds, deletes, and modifies entries inside nested structure without conflicts b = {"a": {}, "d": {"x": 4, "y": 5, "z": 6}, "m": {"x": 7, "y": 8, "z": 9}} l = {"a": {"x": 1, "y": 2}, "d": {"z": 6}, "m": {"x": 17, "y": 18, "z": 9}} r = {"a": {"x": 1, "z": 3}, "d": {"y": 5}, "m": {"x": 17, "y": 8, "z": 19}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == { "a": {"x": 1, "y": 2, "z": 3}, "d": {}, "m": {"x": 17, "y": 18, "z": 19}} assert not any([d.conflict for d in decisions])
def test_autoresolve_dict_use_one_side(): strategies = Strategies({"/foo": "use-base"}) decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 1} strategies = Strategies({"/foo": "use-local"}) decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 2} strategies = Strategies({"/foo": "use-remote"}) decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 3} base2 = {"foo": {"bar": 1}} local2 = {"foo": {"bar": 2}} remote2 = {"foo": {"bar": 3}} conflicted_decisions2 = decide_merge(base2, local2, remote2) strategies = Strategies({"/foo/bar": "use-base"}) decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 1}} strategies = Strategies({"/foo/bar": "use-local"}) decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 2}} strategies = Strategies({"/foo/bar": "use-remote"}) decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 3}}
def test_autoresolve_dict_use_one_side(): strategies = {"/foo": "use-base"} decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 1} strategies = {"/foo": "use-local"} decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 2} strategies = {"/foo": "use-remote"} decisions = autoresolve(base, conflicted_decisions, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 3} base2 = {"foo": {"bar": 1}} local2 = {"foo": {"bar": 2}} remote2 = {"foo": {"bar": 3}} conflicted_decisions2 = decide_merge(base2, local2, remote2) strategies = {"/foo/bar": "use-base"} decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 1}} strategies = {"/foo/bar": "use-local"} decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 2}} strategies = {"/foo/bar": "use-remote"} decisions = autoresolve(base2, conflicted_decisions2, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base2, decisions) == {"foo": {"bar": 3}}
def test_autoresolve_empty_strategies(): """Check that a autoresolve works with empty strategies""" expected_partial_source = [["base\n", "some other\n", "lines\n", "to align\n"]] expected_partial_metadata = [{'myval': 'base'}] expected_partial_outputs = [["base\nsome other\nlines\nto align\n", "output2", "output3"]] base, local, remote, expected_partial = _make_notebook_with_multi_conflicts( expected_partial_source, expected_partial_metadata, expected_partial_outputs ) expected_conflicts = [ { 'action': 'base', 'common_path': ('cells', 0, 'source'), 'conflict': True, 'local_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['local\n']}, {'key': 0, 'length': 1, 'op': 'removerange'}], 'remote_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['remote\n']}, {'key': 0, 'length': 1, 'op': 'removerange'}] }, { 'action': 'base', 'common_path': ('cells', 0, 'outputs', 0, 'data', 'text/plain'), 'conflict': True, 'local_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['local\n']}, {'key': 0, 'length': 1, 'op': 'removerange'}], 'remote_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['remote\n']}, {'key': 0, 'length': 1, 'op': 'removerange'}] }, { 'action': 'base', 'common_path': ('cells', 0, 'metadata', 'myval'), 'conflict': True, 'local_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['local']}, {'key': 0, 'length': 1, 'op': 'removerange'}], 'remote_diff': [{'key': 0, 'op': 'addrange', 'valuelist': ['remote']}, {'key': 0, 'length': 1, 'op': 'removerange'}] } ] # Since we cannot pass directly a strategies object, include copy of relevant code: local_diffs = diff_notebooks(base, local) remote_diffs = diff_notebooks(base, remote) strategies = Strategies() decisions = decide_merge_with_diff( base, local, remote, local_diffs, remote_diffs, strategies) partial = apply_decisions(base, decisions) _check(partial, expected_partial, decisions, expected_conflicts)
def test_deep_merge_lists_delete_no_conflict__currently_expected_failures(): # both remove the same 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) r[i].pop(j) assert l == r decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == r assert not any([d.conflict for d in decisions])
def test_deep_merge_dicts_insert_conflicted(): # local and remote adds the same entry plus an entry each in a new subdict b = {"p": {"b": 1}} l = {"p": {"b": 1}, "n": {"s": 7, "l": 2}} r = {"p": {"b": 1}, "n": {"s": 7, "r": 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_add("n", l["n"])] assert d.remote_diff == [op_add("n", r["n"])]
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_shallow_merge_dicts_delete_no_conflict(): # local removes an entry b = {"b": 1, "a": 3} l = {"b": 1} r = {"b": 1, "a": 3} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1} assert not any([d.conflict for d in decisions]) # remote removes an entry b = {"b": 1, "a": 3} l = {"b": 1, "a": 3} r = {"b": 1} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1} assert not any([d.conflict for d in decisions]) # both remove the same entry b = {"b": 1, "a": 3} l = {"b": 1} r = {"b": 1} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"b": 1} assert not any([d.conflict for d in decisions])
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_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_conflicting_nested_dicts(): # Note: Tests in here were written by writing up the last version # and then copy-pasting and deleting pieces to simplify... # Not pretty for production code but the explicitness is convenient when the tests fail. # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2}} r = {"a": {"x": 3}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}} assert len(decisions) == 1 d = decisions[0] assert d.common_path == ("a",) assert d.local_diff == [op_replace("x", 2)] assert d.remote_diff == [op_replace("x", 3)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {}} l = {"a": {"y": 4}} r = {"a": {"y": 5}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {}} assert len(decisions) == 1 d = decisions[0] assert d.common_path == ("a",) assert d.local_diff == [op_add("y", 4), ] assert d.remote_diff == [op_add("y", 5), ] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}} l = {"a": {"x": 2, "y": 4}} r = {"a": {"x": 3, "y": 5}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}} assert len(decisions) == 2 assert all([d.conflict for d in decisions]) assert all([d.common_path == ("a",) for d in decisions]) assert decisions[0].local_diff == [op_replace("x", 2)] assert decisions[0].remote_diff == [op_replace("x", 3)] assert decisions[1].local_diff == [op_add("y", 4)] assert decisions[1].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, } decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == {"a": {"x": 1}, "d": {"x": 4, "y": 5}} assert len(decisions) == 4 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("d",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("a",) assert decisions[3].common_path == ("a",) assert decisions[0].local_diff == [op_remove("x")] assert decisions[0].remote_diff == [op_replace("x", 5)] assert decisions[1].local_diff == [op_replace("y", 6)] assert decisions[1].remote_diff == [op_remove("y")] assert decisions[2].local_diff == [op_replace("x", 2)] assert decisions[2].remote_diff == [op_replace("x", 3)] assert decisions[3].local_diff == [op_add("y", 4)] assert decisions[3].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}} decisions = decide_merge(b, l, r) assert apply_decisions(b, decisions) == { "a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert len(decisions) == 5 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("m",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("d",) assert decisions[3].common_path == ("a",) assert decisions[4].common_path == ("a",) assert decisions[0].local_diff == [op_replace("x", 17)] assert decisions[0].remote_diff == [op_replace("x", 27)] assert decisions[1].local_diff == [op_remove("x")] assert decisions[1].remote_diff == [op_replace("x", 5)] assert decisions[2].local_diff == [op_replace("y", 6)] assert decisions[2].remote_diff == [op_remove("y")] assert decisions[3].local_diff == [op_replace("x", 2)] assert decisions[3].remote_diff == [op_replace("x", 3)] assert decisions[4].local_diff == [op_add("y", 4)] assert decisions[4].remote_diff == [op_add("y", 5)] # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}, "n": {"q": 9}} r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}, "n": {"q": 19}} decisions = decide_merge(b, l, r) # Note that "n":{} gets added to the merge result even though it's empty assert apply_decisions(b, decisions) == { "a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}} assert len(decisions) == 6 assert all([d.conflict for d in decisions]) assert decisions[0].common_path == ("m",) assert decisions[1].common_path == ("d",) assert decisions[2].common_path == ("d",) assert decisions[3].common_path == ("a",) assert decisions[4].common_path == ("a",) assert decisions[5].common_path == () assert decisions[0].local_diff == [op_replace("x", 17)] assert decisions[0].remote_diff == [op_replace("x", 27)] assert decisions[1].local_diff == [op_remove("x")] assert decisions[1].remote_diff == [op_replace("x", 5)] assert decisions[2].local_diff == [op_replace("y", 6)] assert decisions[2].remote_diff == [op_remove("y")] assert decisions[3].local_diff == [op_replace("x", 2)] assert decisions[3].remote_diff == [op_replace("x", 3)] assert decisions[4].local_diff == [op_add("y", 4)] assert decisions[4].remote_diff == [op_add("y", 5)] assert decisions[5].local_diff == [op_add("n", {"q": 9})] assert decisions[5].remote_diff == [op_add("n", {"q": 19})]
def test_autoresolve_list_conflicting_insertions_mixed(): # local and remote adds an equal entry plus a different entry each # First, test when insertions DO NOT chunk together: b = [1, 9] l = [1, 2, 9, 11] r = [1, 3, 9, 11] decisions = decide_merge(b, l, r) # Check strategyless resolution strategies = {} resolved = autoresolve(b, decisions, strategies) expected_partial = [1, 9, 11] assert apply_decisions(b, resolved) == expected_partial assert len(resolved) == 2 assert resolved[0].conflict assert not resolved[1].conflict strategies = {"/*": "use-local"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = {"/*": "use-remote"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = {"/*": "use-base"} resolved = autoresolve(b, decisions, strategies) # Strategy is only applied to conflicted decisions: assert apply_decisions(b, resolved) == expected_partial assert not any(d.conflict for d in resolved) strategies = {"/*": "join"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 9, 11] assert not any(d.conflict for d in resolved) strategies = {"/*": "clear-parent"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved) # Next, test when insertions DO chunk together: b = [1, 9] l = [1, 2, 7, 9] r = [1, 3, 7, 9] decisions = decide_merge(b, l, r) # Check strategyless resolution strategies = {} resolved = autoresolve(b, decisions, strategies) expected_partial = [1, 7, 9] assert apply_decisions(b, resolved) == expected_partial assert resolved == decisions # Not able to resolve anything strategies = {"/*": "use-local"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = {"/*": "use-remote"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = {"/*": "use-base"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == expected_partial assert not any(d.conflict for d in resolved) strategies = {"/*": "join"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 7, 9] assert not any(d.conflict for d in resolved) strategies = {"/*": "clear-parent"} resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)
def test_autoresolve_list_conflicting_insertions_mixed(): # local and remote adds an equal entry plus a different entry each # First, test when insertions DO NOT chunk together: b = [1, 9] l = [1, 2, 9, 11] r = [1, 3, 9, 11] decisions = decide_merge(b, l, r) # Check strategyless resolution strategies = Strategies({}) resolved = autoresolve(b, decisions, strategies) expected_partial = [1, 9, 11] assert apply_decisions(b, resolved) == expected_partial assert len(resolved) == 2 assert resolved[0].conflict assert not resolved[1].conflict strategies = Strategies({"/*": "use-local"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-remote"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-base"}) resolved = autoresolve(b, decisions, strategies) # Strategy is only applied to conflicted decisions: assert apply_decisions(b, resolved) == expected_partial assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "union"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 9, 11] assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "clear-all"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved) # Next, test when insertions DO chunk together: b = [1, 9] l = [1, 2, 7, 9] r = [1, 3, 7, 9] decisions = decide_merge(b, l, r) # Check strategyless resolution strategies = Strategies({}) resolved = autoresolve(b, decisions, strategies) expected_partial = [1, 7, 9] assert apply_decisions(b, resolved) == expected_partial assert resolved == decisions # Not able to resolve anything strategies = Strategies({"/*": "use-local"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-remote"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-base"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == expected_partial assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "union"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 7, 9] assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "clear-all"}) resolved = autoresolve(b, decisions, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)