def test_decide_merge_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) # Assert that generic merge gives conflict strategies = Strategies() decisions = decide_merge_with_diff(b, l, r, ld, rd, strategies) assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=[ '/*/a/transient' ]) decisions = decide_merge_with_diff(b, l, r, ld, rd, strategies) assert apply_decisions(b, decisions) == r assert not any(d.conflict for d in decisions)
def test_decide_merge_simple_list_insert_conflict_resolution(): # local and remote adds an entry each b = [1] l = [1, 2] r = [1, 3] strategies = Strategies({"/*": "use-local"}) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == l assert not any(d.conflict for d in decisions) strategies = Strategies({"/*": "use-remote"}) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == r assert not any(d.conflict for d in decisions) strategies = Strategies({"/*": "use-base"}) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == b assert not any(d.conflict for d in decisions) strategies = Strategies({"/": "clear-all"}) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == [] assert not any(d.conflict for d in decisions)
def test_decide_merge_list_conflicting_insertions_in_chunks(): # Next, test when insertions DO chunk together: b = [1, 9] l = [1, 2, 7, 9] r = [1, 3, 7, 9] # Check strategyless resolution strategies = Strategies({}) resolved = decide_merge(b, l, r, strategies) expected_partial = [1, 7, 9] assert apply_decisions(b, resolved) == expected_partial strategies = Strategies({"/*": "use-local"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-remote"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-base"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == expected_partial assert not any(d.conflict for d in resolved) strategies = Strategies({"/": "clear-all"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)
def test_decide_merge_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) # Assert that generic merge gives conflict strategies = Strategies() decisions = decide_merge_with_diff(b, l, r, ld, rd, strategies) assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=['/*/a/transient']) decisions = decide_merge_with_diff(b, l, r, ld, rd, strategies) assert apply_decisions(b, decisions) == r assert not any(d.conflict for d in decisions)
def test_decide_merge_strategy_clear_all(): base = {"foo": [1, 2]} local = {"foo": [1, 4, 2]} remote = {"foo": [1, 3, 2]} strategies = Strategies({"/foo": "clear-all"}) decisions = decide_merge(base, local, remote, strategies) assert apply_decisions(base, decisions) == {"foo": []} base = {"foo": [1, 2]} local = {"foo": [1, 4, 2]} remote = {"foo": [1, 2, 3]} strategies = Strategies({"/foo": "clear-all"}) decisions = decide_merge(base, local, remote, strategies) assert apply_decisions(base, decisions) == {"foo": [1, 4, 2, 3]}
def test_decide_merge_strategy_remove(): base = {"foo": [1, 2]} local = {"foo": [1, 4, 2]} remote = {"foo": [1, 3, 2]} strategies = Strategies({"/foo": "remove"}) decisions = decide_merge(base, local, remote, strategies) assert apply_decisions(base, decisions) == {"foo": [1, 2]} assert decisions[0].local_diff != [] assert decisions[0].remote_diff != [] strategies = Strategies({}) decisions = decide_merge(base, local, remote, strategies) assert apply_decisions(base, decisions) == {"foo": [1, 2]} assert decisions[0].local_diff != [] assert decisions[0].remote_diff != []
def merge(base, local, remote, args=None): """Merge changes introduced by notebooks local and remote from a shared ancestor base. Return new (partially) merged notebook and unapplied diffs from the local and remote side. """ if args and args.log_level == "DEBUG": # log pretty-print config object: config = PrettyPrintConfig() for (name, nb) in [("base", base), ("local", local), ("remote", remote)]: nbdime.log.debug("In merge, input %s notebook:", name) config.out = StringIO() pretty_print_notebook(nb, config) nbdime.log.debug(config.out.getvalue()) decisions = decide_merge(base, local, remote, args) merged = apply_decisions(base, decisions) if args and args.log_level == "DEBUG": nbdime.log.debug("In merge, merged notebook:") config.out = StringIO() pretty_print_notebook(merged, config) nbdime.log.debug(config.out.getvalue()) nbdime.log.debug("End merge") return merged, decisions
def test_decide_merge_strategy_clear2(): base = {"foo": "1"} local = {"foo": "2"} remote = {"foo": "3"} strategies = Strategies({"/foo": "clear"}) decisions = decide_merge(base, local, remote, strategies) #assert decisions == [] assert apply_decisions(base, decisions) == {"foo": ""} assert not any([d.conflict for d in decisions])
def test_decide_merge_dict_transients(): # Setup transient difference in base and local, deletion in remote b = {'a': {'transient': 22}} l = {'a': {'transient': 242}} r = {} # Assert that generic merge gives conflict strategies = Strategies() decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=['/a/transient']) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == r assert not any(d.conflict for d in decisions)
def test_decide_merge_list_conflicting_insertions_in_chunks__union(): # Next, test when insertions DO chunk together: b = [1, 9] l = [1, 2, 7, 9] r = [1, 3, 7, 9] strategies = Strategies({"/": "union"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 7, 9] assert not any(d.conflict for d in resolved)
def test_decide_merge_simple_list_insert_conflict_resolution__union(): # local and remote adds an entry each b = [1] l = [1, 2] r = [1, 3] strategies = Strategies({"/": "union"}) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == [1, 2, 3] assert not any(d.conflict for d in decisions)
def test_decide_merge_strategy_clear1(): """Check strategy "clear" in various cases.""" # One level dict, clearing item value (think foo==execution_count) base = {"foo": 1} local = {"foo": 2} remote = {"foo": 3} strategies = Strategies({"/foo": "clear"}) decisions = decide_merge(base, local, remote, strategies) assert apply_decisions(base, decisions) == {"foo": None} assert not any([d.conflict for d in decisions])
def test_decide_merge_list_conflicting_insertions_separate_chunks__union(): # 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] strategies = Strategies({"/": "union"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == [1, 2, 3, 9, 11] assert not any(d.conflict for d in resolved)
def test_decide_merge_dict_transients(): # Setup transient difference in base and local, deletion in remote b = {'a': {'transient': 22}} l = {'a': {'transient': 242}} r = {} # Assert that generic merge gives conflict strategies = Strategies() decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == b assert len(decisions) == 1 assert decisions[0].conflict # Supply transient list to autoresolve, and check that transient is ignored strategies = Strategies(transients=[ '/a/transient' ]) decisions = decide_merge(b, l, r, strategies) assert apply_decisions(b, decisions) == r assert not any(d.conflict for d in decisions)
def test_decide_merge_list_conflicting_insertions_separate_chunks_v2(): # 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] # Check strategyless resolution strategies = Strategies({}) resolved = decide_merge(b, l, r, 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
def test_decide_merge_list_conflicting_insertions_separate_chunks_v1(): # 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] # Check strategyless resolution strategies = Strategies({}) resolved = decide_merge(b, l, r, 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 = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == l assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-remote"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == r assert not any(d.conflict for d in resolved) strategies = Strategies({"/*": "use-base"}) resolved = decide_merge(b, l, r, 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({"/": "clear-all"}) resolved = decide_merge(b, l, r, strategies) assert apply_decisions(b, resolved) == [] assert not any(d.conflict for d in resolved)
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_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_decide_merge_strategy_use_foo_on_dict_items(): base = {"foo": 1} local = {"foo": 2} remote = {"foo": 3} strategies = Strategies({"/foo": "use-base"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 1} strategies = Strategies({"/foo": "use-local"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 2} strategies = Strategies({"/foo": "use-remote"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": 3} base = {"foo": {"bar": 1}} local = {"foo": {"bar": 2}} remote = {"foo": {"bar": 3}} strategies = Strategies({"/foo/bar": "use-base"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": {"bar": 1}} strategies = Strategies({"/foo/bar": "use-local"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": {"bar": 2}} strategies = Strategies({"/foo/bar": "use-remote"}) decisions = decide_merge(base, local, remote, strategies) assert not any([d.conflict for d in decisions]) assert apply_decisions(base, decisions) == {"foo": {"bar": 3}}
def test_apply_merge_empty(): decisions = [] base = {"hello": "world"} assert base == apply_decisions(base, decisions)