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_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 xtest_autoresolve_fail(): # These are reused in all tests below args = None base = { "foo": 1 } local = { "foo": 2 } remote = { "foo": 3 } # Check that "fail" strategy results in proper exception raised strategies = { "/foo": "fail" } with pytest.raises(RuntimeError): autoresolve(base, diff(base, local), diff(base, remote), args, strategies, "")
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 xtest_autoresolve_invalidate(): # These are reused in all tests below args = None base = { "foo": 1 } local = { "foo": 2 } remote = { "foo": 3 } # Check strategies invalidate and use-* strategies = { "/foo": "invalidate" } merged, local_conflicts, remote_conflicts = autoresolve(base, diff(base, local), diff(base, remote), args, strategies, "") assert merged == { "foo": None } assert local_conflicts == [] assert remote_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_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_diff_to_json_patch(): a = [2, 3, 4] b = [1, 2, 4, 6] d = diff(a, b) assert to_json_patch(d) == [{ 'op': 'add', 'path': '/0', 'value': 1 }, { 'op': 'remove', 'path': '/2' }, { 'op': 'add', 'path': '/3', 'value': 6 }] try: import jsonpatch except: jsonpatch = None pytest.xfail("Not comparing to jsonpatch") if jsonpatch: assert to_json_patch(d) == jsonpatch.make_patch(a, b).patch
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_apply_merge_on_dicts(): base = {"metadata": {"a": {"ting": 123}, "b": {"tang": 456}}} local = copy.deepcopy(base) local["metadata"]["a"]["ting"] += 1 remote = copy.deepcopy(base) remote["metadata"]["a"]["ting"] -= 1 bld = diff(base, local) brd = diff(base, remote) path, (bld, brd) = ensure_common_path((), [bld, brd]) merge_decisions = [create_decision_item(action="remote", common_path=path, local_diff=bld, remote_diff=brd)] assert remote == apply_decisions(base, merge_decisions)
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_apply_merge_on_dicts(): base = {"metadata": {"a": {"ting": 123}, "b": {"tang": 456}}} local = copy.deepcopy(base) local["metadata"]["a"]["ting"] += 1 remote = copy.deepcopy(base) remote["metadata"]["a"]["ting"] -= 1 bld = diff(base, local) brd = diff(base, remote) path, (bld, brd) = ensure_common_path((), [bld, brd]) merge_decisions = [ create_decision_item(action="remote", common_path=path, local_diff=bld, remote_diff=brd) ] assert remote == apply_decisions(base, merge_decisions)
def test_diff_to_json(): a = { "foo": [1,2,3], "bar": {"ting": 7, "tang": 123 } } b = { "foo": [1,3,4], "bar": {"tang": 126, "hello": "world" } } d1 = diff(a, b) d2 = to_clean_dicts(d1) assert len(d2) == len(d1) assert all(len(e2) == len(e1) for e1, e2 in zip(d1, d2)) j = json.dumps(d1) d3 = json.loads(j) assert len(d3) == len(d1) assert all(len(e3) == len(e1) for e1, e3 in zip(d1, d3)) assert d2 == d3
def test_diff_to_json(): a = {"foo": [1, 2, 3], "bar": {"ting": 7, "tang": 123}} b = {"foo": [1, 3, 4], "bar": {"tang": 126, "hello": "world"}} d1 = diff(a, b) d2 = to_clean_dicts(d1) assert len(d2) == len(d1) assert all(len(e2) == len(e1) for e1, e2 in zip(d1, d2)) j = json.dumps(d1) d3 = json.loads(j) assert len(d3) == len(d1) assert all(len(e3) == len(e1) for e1, e3 in zip(d1, d3)) assert d2 == d3
def test_diff_to_json_patch(): a = [2, 3, 4] b = [1, 2, 4, 6] d = diff(a, b) assert to_json_patch(d) == [ {'op': 'add', 'path': '/0', 'value': 1}, {'op': 'remove', 'path': '/2'}, {'op': 'add', 'path': '/3', 'value': 6} ] try: import jsonpatch except: jsonpatch = None pytest.xfail("Not comparing to jsonpatch") if jsonpatch: assert to_json_patch(d) == jsonpatch.make_patch(a, b).patch
def xtest_autoresolve_use_one(): # These are reused in all tests below args = None base = { "foo": 1 } local = { "foo": 2 } remote = { "foo": 3 } strategies = { "/foo": "use-base" } merged, local_conflicts, remote_conflicts = autoresolve(base, diff(base, local), diff(base, remote), args, strategies, "") assert local_conflicts == [] assert remote_conflicts == [] assert merged == { "foo": 1 } strategies = { "/foo": "use-local" } merged, local_conflicts, remote_conflicts = autoresolve(base, diff(base, local), diff(base, remote), args, strategies, "") assert merged == { "foo": 2 } assert local_conflicts == [] assert remote_conflicts == [] strategies = { "/foo": "use-remote" } merged, local_conflicts, remote_conflicts = autoresolve(base, diff(base, local), diff(base, remote), args, strategies, "") assert merged == { "foo": 3 } assert local_conflicts == [] assert remote_conflicts == []
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_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 check_diff_and_patch(a, b): "Check that patch(a, diff(a,b)) reproduces b." d = diff(a, b) assert is_valid_diff(d) assert patch(a, d) == b
def test_validate_array_diff(diff_validator): a = [2, 3, 4] b = [1, 2, 4, 6] d = diff(a, b) diff_validator.validate(d)
def post(self): base_nb = self.get_notebook_argument('base') body = json.loads(escape.to_unicode(self.request.body)) base_selected_execution = body['base_selected_execution'] remote_selected_execution = body['remote_selected_execution'] cell_index = body['cell_index'] base_notebook = {} remote_notebook = {} base_notebook['metadata'] = base_nb['metadata'] remote_notebook['metadata'] = base_nb['metadata'] base_notebook['nbformat'] = base_nb['nbformat'] remote_notebook['nbformat'] = base_nb['nbformat'] base_notebook['nbformat_minor'] = base_nb['nbformat_minor'] remote_notebook['nbformat_minor'] = base_nb['nbformat_minor'] base_notebook['cells'] = [] remote_notebook['cells'] = [] for cell_i, cell_node in enumerate(base_nb['cells']): base_prov_obj = {} remote_prov_obj = {} if (int(cell_i) == int(cell_index)): if 'provenance' in cell_node['metadata']: provenance = cell_node['metadata']['provenance'] base_selected_execution_prov = provenance[ base_selected_execution] remote_selected_execution_prov = provenance[ remote_selected_execution] base_prov_obj['source'] = base_selected_execution_prov[ 'source'] remote_prov_obj['source'] = remote_selected_execution_prov[ 'source'] base_prov_obj['outputs'] = base_selected_execution_prov[ 'outputs'] remote_prov_obj[ 'outputs'] = remote_selected_execution_prov['outputs'] base_prov_obj['execution_count'] = cell_node[ 'execution_count'] remote_prov_obj['execution_count'] = cell_node[ 'execution_count'] base_prov_obj['metadata'] = {} remote_prov_obj['metadata'] = {} base_prov_obj['cell_type'] = cell_node['cell_type'] remote_prov_obj['cell_type'] = cell_node['cell_type'] base_notebook['cells'] = [base_prov_obj] remote_notebook['cells'] = [remote_prov_obj] d = nbdime.diff(provenance[base_selected_execution], provenance[remote_selected_execution]) else: base_notebook = base_nb remote_notebook = base_nb try: thediff = nbdime.diff_notebooks(base_notebook, remote_notebook) except Exception: nbdime.log.exception('Error diffing documents:') raise web.HTTPError(500, 'Error while attempting to diff documents') data = { 'base': base_notebook, 'diff': thediff, } self.finish(data)
def test_validate_obj_diff(diff_validator): a = {"foo": [1, 2, 3], "bar": {"ting": 7, "tang": 123}} b = {"foo": [1, 3, 4], "bar": {"tang": 126, "hello": "world"}} d = diff(a, b) diff_validator.validate(d)
def test_validate_array_diff(): a = [2, 3, 4] b = [1, 2, 4, 6] d = diff(a, b) validator.validate(d)
def test_validate_obj_diff(): a = {"foo": [1, 2, 3], "bar": {"ting": 7, "tang": 123}} b = {"foo": [1, 3, 4], "bar": {"tang": 126, "hello": "world"}} d = diff(a, b) validator.validate(d)
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_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_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_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