Example #1
0
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)
Example #2
0
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)
Example #3
0
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, "")
Example #4
0
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 == []
Example #5
0
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 == []
Example #6
0
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 == []
Example #7
0
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)]
Example #8
0
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:])]
Example #9
0
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:])]
Example #10
0
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
Example #11
0
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)]
Example #12
0
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)
Example #13
0
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]])]
Example #14
0
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
Example #17
0
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
Example #19
0
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)
Example #21
0
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)
            ]),
        ]
Example #22
0
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
Example #23
0
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)
Example #25
0
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)
Example #26
0
def test_validate_array_diff():
    a = [2, 3, 4]
    b = [1, 2, 4, 6]
    d = diff(a, b)

    validator.validate(d)
Example #27
0
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)
Example #28
0
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_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
Example #31
0
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