Ejemplo n.º 1
0
def test_patch_list():
    # Test +, single item insertion
    assert patch([], [op_add(0, 3)]) == [3]
    assert patch([], [op_add(0, 3), op_add(0, 4)]) == [3, 4]
    assert patch([], [op_add(0, 3), op_add(0, 4), op_add(0, 5)]) == [3, 4, 5]

    # Test -, single item deletion
    assert patch([3], [op_remove(0)]) == []
    assert patch([5, 6, 7], [op_remove(0)]) == [6, 7]
    assert patch([5, 6, 7], [op_remove(1)]) == [5, 7]
    assert patch([5, 6, 7], [op_remove(2)]) == [5, 6]
    assert patch([5, 6, 7], [op_remove(0), op_remove(2)]) == [6]

    # Test :, single item replace
    pass

    # Test !, item patch
    assert patch(["hello", "world"], [
        op_patch(0, [op_patch(0, [op_replace(0, "H")])]),
        op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])
    ]) == ["Hello", "World"]

    # Test ++, sequence insertion
    assert patch(
        [], [op_addrange(0, [3, 4]),
             op_add(0, 5),
             op_addrange(0, [6, 7])]) == [3, 4, 5, 6, 7]

    # Test --, sequence deletion
    assert patch([5, 6, 7, 8], [op_removerange(0, 2)]) == [7, 8]
    assert patch([5, 6, 7, 8], [op_removerange(1, 2)]) == [5, 8]
    assert patch([5, 6, 7, 8], [op_removerange(2, 2)]) == [5, 6]
Ejemplo n.º 2
0
def test_patch_list():
    # Test +, single item insertion
    assert patch([], [op_add(0, 3)]) == [3]
    assert patch([], [op_add(0, 3), op_add(0, 4)]) == [3, 4]
    assert patch([], [op_add(0, 3), op_add(0, 4), op_add(0, 5)]) == [3, 4, 5]

    # Test -, single item deletion
    assert patch([3], [op_remove(0)]) == []
    assert patch([5, 6, 7], [op_remove(0)]) == [6, 7]
    assert patch([5, 6, 7], [op_remove(1)]) == [5, 7]
    assert patch([5, 6, 7], [op_remove(2)]) == [5, 6]
    assert patch([5, 6, 7], [op_remove(0), op_remove(2)]) == [6]

    # Test :, single item replace
    pass

    # Test !, item patch
    assert patch(["hello", "world"], [op_patch(0, [op_patch(0, [op_replace(0, "H")])]),
                                      op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])]) == ["Hello", "World"]

    # Test ++, sequence insertion
    assert patch([], [op_addrange(0, [3, 4]), op_add(0, 5), op_addrange(0, [6, 7])]) == [3, 4, 5, 6, 7]

    # Test --, sequence deletion
    assert patch([5, 6, 7, 8], [op_removerange(0, 2)]) == [7, 8]
    assert patch([5, 6, 7, 8], [op_removerange(1, 2)]) == [5, 8]
    assert patch([5, 6, 7, 8], [op_removerange(2, 2)]) == [5, 6]
Ejemplo n.º 3
0
def test_merge_insert_cells_around_conflicting_cell():
    # Modifying an original cell and inserting a new cell on both sides
    source = [
        "def foo(x, y):\n",
        "    z = x * y\n",
        "    return z\n",
    ]
    local = [["new local cell\n"], source + ["local\n"]]
    base = [source]
    remote = [source + ["remote\n"], ["new remote cell\n"]]
    # Use mergetool strategy:
    merge_args = copy.deepcopy(args)
    merge_args.merge_strategy = "mergetool"
    if 0:
        # This is how it would look if neither source or cell inserts resulted
        # in conflicts:
        expected_partial = [
            local[0], source + ["local\n", "remote\n"], remote[1]
        ]
        expected_conflicts = []
    elif 1:
        # This is how it would look if source inserts but not cell inserts
        # resulted in conflicts:
        expected_partial = [local[0], source, remote[1]]
        expected_conflicts = [{
            "common_path": ("cells", 0, "source"),
            "local_diff": [op_addrange(len(source), ["local\n"])],
            "remote_diff": [op_addrange(len(source), ["remote\n"])]
        }]
    else:
        # In current behaviour:
        # - base cell 0 is aligned correctly (this is the notebook diff heuristics)
        # - conflicting edits of base cell 0 detected
        # - local insert before cell 0 is treated as part of conflict on base cell 0
        # - remote insert after cell 0 is not treated as part of conflict
        # FIXME: This may not be the exact behaviour we want:
        # - For source, we might want both inserts to be part of the conflict.
        #   (if so, fix in generic merge and chunk collection)
        # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?)
        # - Figure out the best behaviour and make it happen!
        expected_partial = [source, remote[1]]
        expected_conflicts = [{
            "common_path": ("cells", ),
            "local_diff": [
                op_addrange(
                    0, [nbformat.v4.new_code_cell(source=["new local cell"])]),
                op_patch(0, [
                    op_patch("source",
                             [op_addrange(len("".join(source)), "local\n")])
                ]),
            ],
            "remote_diff": [
                op_patch(0, [
                    op_patch('source',
                             [op_addrange(len("".join(source)), "remote\n")])
                ])
            ]
        }]
    _check_sources(base, local, remote, expected_partial, expected_conflicts,
                   merge_args)
Ejemplo n.º 4
0
def test_patch_dict():
    # Test +, single item insertion
    assert patch({}, [op_add("d", 4)]) == {"d": 4}
    assert patch({"a": 1}, [op_add("d", 4)]) == {"a": 1, "d": 4}

    #assert patch({"d": 1}, [op_add("d", 4)]) == {"d": 4} # currently triggers assert, raise exception or allow?

    # Test -, single item deletion
    assert patch({"a": 1}, [op_remove("a")]) == {}
    assert patch({"a": 1, "b": 2}, [op_remove("a")]) == {"b": 2}

    # Test :, single item replace
    assert patch({"a": 1, "b": 2}, [op_replace("a", 3)]) == {"a": 3, "b": 2}
    assert patch({
        "a": 1,
        "b": 2
    }, [op_replace("a", 3), op_replace("b", 5)]) == {
        "a": 3,
        "b": 5
    }

    # Test !, item patch
    subdiff = [
        op_patch(0, [op_patch(0, [op_replace(0, "H")])]),
        op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])
    ]
    assert patch({
        "a": ["hello", "world"],
        "b": 3
    }, [op_patch("a", subdiff)]) == {
        "a": ["Hello", "World"],
        "b": 3
    }
Ejemplo n.º 5
0
def test_merge_builder_ensures_common_path():
    b = MergeDecisionBuilder()
    b.conflict(("a", "b"), [op_patch("c", [op_remove("d")])],
               [op_patch("c", [op_remove("e")])])
    assert len(b.decisions) == 1
    assert b.decisions[0].common_path == ("a", "b", "c")
    assert b.decisions[0].local_diff == [op_remove("d")]
    assert b.decisions[0].remote_diff == [op_remove("e")]
Ejemplo n.º 6
0
def test_merge_inserts_within_deleted_range():
    # Multiple inserts within a deleted range
    base = """
def f(x):
    return x**2

def g(x):
    return x + 2
"""

    # Insert foo and bar
    local = """
def foo(y):
    return y / 3

def f(x):
    return x**2

def bar(y):
    return y - 3

def g(x):
    return x + 2
"""

    remote = ""  # Delete all

    if 0:
        # This is quite optimistic and would require employing aggressive
        # attempts at automatic resolution beyond what git and meld do:
        expected_partial = """
def foo(y):
    return y / 3

def bar(y):
    return y - 3
"""
    else:
        expected_partial = base
        expected_conflicts = [{
            "common_path": ("cells", ),
            "local_diff": [
                op_patch("cells", [
                    op_addrange(0, [nbformat.v4.new_code_cell(local)]),
                    op_removerange(0, 1)
                ])
            ],
            "remote_diff": [
                op_patch("cells", [
                    op_addrange(0, [nbformat.v4.new_code_cell(remote)]),
                    op_removerange(0, 1)
                ])
            ]
        }]
    _check_sources(base, local, remote, expected_partial, expected_conflicts)

    # Keep it failing
    assert False
Ejemplo n.º 7
0
def test_merge_builder_ensures_common_path():
    b = MergeDecisionBuilder()
    b.conflict(("a", "b"),
               [op_patch("c", [op_remove("d")])],
               [op_patch("c", [op_remove("e")])])
    assert len(b.decisions) == 1
    assert b.decisions[0].common_path == ("a", "b", "c")
    assert b.decisions[0].local_diff == [op_remove("d")]
    assert b.decisions[0].remote_diff == [op_remove("e")]
Ejemplo n.º 8
0
def test_merge_insert_cells_around_conflicting_cell():
    # Modifying an original cell and inserting a new cell on both sides
    source = [
        "def foo(x, y):\n",
        "    z = x * y\n",
        "    return z\n",
        ]
    local = [["new local cell\n"],
             source + ["local\n"]]
    base = [source]
    remote = [source + ["remote\n"],
              ["new remote cell\n"]]
    # Use mergetool strategy:
    merge_args = copy.deepcopy(args)
    merge_args.merge_strategy = "mergetool"
    if 0:
        # This is how it would look if neither source or cell inserts resulted
        # in conflicts:
        expected_partial = [local[0],
                            source + ["local\n", "remote\n"],
                            remote[1]]
        expected_conflicts = []
    elif 1:
        # This is how it would look if source inserts but not cell inserts
        # resulted in conflicts:
        expected_partial = [local[0], source, remote[1]]
        expected_conflicts = [{
            "common_path": ("cells", 0, "source"),
            "local_diff": [op_addrange(len(source), ["local\n"])],
            "remote_diff": [op_addrange(len(source), ["remote\n"])]
        }]
    else:
        # In current behaviour:
        # - base cell 0 is aligned correctly (this is the notebook diff heuristics)
        # - conflicting edits of base cell 0 detected
        # - local insert before cell 0 is treated as part of conflict on base cell 0
        # - remote insert after cell 0 is not treated as part of conflict
        # FIXME: This may not be the exact behaviour we want:
        # - For source, we might want both inserts to be part of the conflict.
        #   (if so, fix in generic merge and chunk collection)
        # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?)
        # - Figure out the best behaviour and make it happen!
        expected_partial = [source, remote[1]]
        expected_conflicts = [{
            "common_path": ("cells",),
            "local_diff": [
                op_addrange(0, [nbformat.v4.new_code_cell(
                    source=["new local cell"])]),
                op_patch(0, [op_patch("source", [
                    op_addrange(len("".join(source)), "local\n")])]),
            ],
            "remote_diff": [op_patch(0, [op_patch('source', [
                op_addrange(len("".join(source)), "remote\n")])])]
        }]
    _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
Ejemplo n.º 9
0
def test_pop_patch_single_level():
    md = MergeDecision(common_path=("a", "b"),
                       action="base",
                       conflict=True,
                       local_diff=[op_patch("c", [op_remove("d")])],
                       remote_diff=[op_patch("c", [op_remove("e")])])
    dec = pop_patch_decision(md)
    assert dec is not None
    assert dec.common_path == ("a", "b", "c")
    assert dec.local_diff == [op_remove("d")]
    assert dec.remote_diff == [op_remove("e")]
Ejemplo n.º 10
0
def test_build_diffs_unsorted():
    b = MergeDecisionBuilder()
    b.onesided((), [op_remove('a')], None)
    b.onesided(('b',), [op_remove('j')], None)
    b.onesided(('c',), [op_remove('k')], None)
    b.onesided(('d',), [op_remove('l')], None)
    base = dict(a=1, b=dict(i=2), c=dict(j=3), d=dict(k=4))
    diff = build_diffs(base, b.decisions, 'local')
    assert len(diff) == 4
    assert diff[0] == op_remove('a')
    assert diff[1] == op_patch('b', [op_remove('j')])
    assert diff[2] == op_patch('c', [op_remove('k')])
    assert diff[3] == op_patch('d', [op_remove('l')])
Ejemplo n.º 11
0
def test_build_diffs_unsorted():
    b = MergeDecisionBuilder()
    b.onesided((), [op_remove('a')], None)
    b.onesided(('b',), [op_remove('j')], None)
    b.onesided(('c',), [op_remove('k')], None)
    b.onesided(('d',), [op_remove('l')], None)
    base = dict(a=1, b=dict(i=2), c=dict(j=3), d=dict(k=4))
    diff = build_diffs(base, b.decisions, 'local')
    assert len(diff) == 4
    assert diff[0] == op_remove('a')
    assert diff[1] == op_patch('b', [op_remove('j')])
    assert diff[2] == op_patch('c', [op_remove('k')])
    assert diff[3] == op_patch('d', [op_remove('l')])
Ejemplo n.º 12
0
def test_pop_patch_single_level():
    md = MergeDecision(
        common_path=("a", "b"),
        action="base",
        conflict=True,
        local_diff=[op_patch("c", [op_remove("d")])],
        remote_diff=[op_patch("c", [op_remove("e")])]
    )
    dec = pop_patch_decision(md)
    assert dec is not None
    assert dec.common_path == ("a", "b", "c")
    assert dec.local_diff == [op_remove("d")]
    assert dec.remote_diff == [op_remove("e")]
Ejemplo n.º 13
0
def test_merge_inserts_within_deleted_range():
    # Multiple inserts within a deleted range
    base = """
def f(x):
    return x**2

def g(x):
    return x + 2
"""

    # Insert foo and bar
    local = """
def foo(y):
    return y / 3

def f(x):
    return x**2

def bar(y):
    return y - 3

def g(x):
    return x + 2
"""

    remote = ""  # Delete all

    if 0:
        # This is quite optimistic and would require employing aggressive
        # attempts at automatic resolution beyond what git and meld do:
        expected_partial = """
def foo(y):
    return y / 3

def bar(y):
    return y - 3
"""
    else:
        expected_partial = base
        expected_conflicts = [{
            "common_path": ("cells",),
            "local_diff": [op_patch("cells", [op_addrange(0, [
                nbformat.v4.new_code_cell(local)]), op_removerange(0, 1)])],
            "remote_diff": [op_patch("cells", [op_addrange(0, [
                nbformat.v4.new_code_cell(remote)]), op_removerange(0, 1)])]
        }]
    _check_sources(base, local, remote, expected_partial, expected_conflicts)

    # Keep it failing
    assert False
Ejemplo n.º 14
0
def test_merge_insert_cells_around_conflicting_cell():
    # Modifying an original cell and inserting a new cell on both sides
    source = [
        "def foo(x, y):",
        "    z = x * y",
        "    return z",
    ]
    local = [["new local cell"], source + ["local"]]
    base = [source]
    remote = [source + ["remote"], ["new remote cell"]]
    if 0:
        # This is how it would look if neither source or cell inserts resulted in conflicts:
        expected_partial = [["new local cell"], source + ["local", "remote"],
                            ["new remote cell"]]
        expected_lco = []
        expected_rco = []
    elif 0:
        # This is how it would look if source inserts but not cell inserts resulted in conflicts:
        expected_partial = [local[0], source, remote[1]]
        expected_lco = _patch_cell_source(
            1, [op_addrange(len("\n".join(source)), "\nlocal")])
        expected_rco = _patch_cell_source(
            1, [op_addrange(len("\n".join(source)), "\nremote")])
    else:
        # In current behaviour:
        # - base cell 0 is aligned correctly (this is the notebook diff heuristics)
        # - conflicting edits of base cell 0 detected
        # - local insert before cell 0 is treated as part of conflict on base cell 0
        # - remote insert after cell 0 is not treated as part of conflict
        # FIXME: This may not be the exact behaviour we want:
        # - For source, we might want both inserts to be part of the conflict.
        #   (if so, fix in generic merge and chunk collection)
        # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?)
        # - Figure out the best behaviour and make it happen!
        expected_partial = [source, remote[1]]
        expected_lco = [
            op_patch("cells", [
                op_addrange(
                    0, [nbformat.v4.new_code_cell(source=["new local cell"])]),
                op_patch(0, [
                    op_patch("source",
                             [op_addrange(len("\n".join(source)), "\nlocal")])
                ]),
            ])
        ]
        expected_rco = _patch_cell_source(
            0, [op_addrange(len("\n".join(source)), "\nremote")])
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)
Ejemplo n.º 15
0
def pick_merge_decision(base, dec):
    if dec.action is None or dec.action == "base":
        di = None
    elif dec.action == "local" or dec.action == "either":
        di = dec.local_diff
    elif dec.action == "remote":
        di = dec.remote_diff
    elif dec.action == "custom":
        di = dec.custom_diff
    else:
        raise ValueError("Unknown action {}".format(dec.action))

    if di is not None:
        # Parse common path
        keys = [k for k in dec.common_path.split("/") if k != ""]
        sub = base
        for k in keys:
            if isinstance(sub, list):
                k = int(k)
            sub = sub[k]
        # "/cells" -> sub = base[cells], sub is a list
        # patch

        # Add patch entries
        base_diff = di
        for k in reversed(keys):
            if is_int(k):
                k = int(k)
            base_diff = op_patch(k, base_diff)
        # Apply patch
        base = patch(base, base_diff)

    return base
Ejemplo n.º 16
0
def pick_merge_decision(base, dec):
    if dec.action is None or dec.action == "base":
        di = None
    elif dec.action == "local" or dec.action == "either":
        di = dec.local_diff
    elif dec.action == "remote":
        di = dec.remote_diff
    elif dec.action == "custom":
        di = dec.custom_diff
    else:
        raise ValueError("Unknown action {}".format(dec.action))

    if di is not None:
        # Parse common path
        keys = [k for k in dec.common_path.split("/") if k != ""]
        sub = base
        for k in keys:
            if isinstance(sub, list):
                k = int(k)
            sub = sub[k]
        # "/cells" -> sub = base[cells], sub is a list
        # patch

        # Add patch entries
        base_diff = di
        for k in reversed(keys):
            if is_int(k):
                k = int(k)
            base_diff = op_patch(k, base_diff)
        # Apply patch
        base = patch(base, base_diff)

    return base
Ejemplo n.º 17
0
def test_merge_input_strategy_inline_source_conflict():
    # Conflicting cell inserts at same location as removing old cell
    local = [["local\n", "some other\n", "lines\n", "to align\n"]]
    base = [["base\n", "some other\n", "lines\n", "to align\n"]]
    remote = [["remote\n", "some other\n", "lines\n", "to align\n"]]
    # Ideal case:
    expected_partial = [[
       "<<<<<<< local\n",
       "local\n",
       #"||||||| base\n",
       #"base\n",
       "=======\n",
       "remote\n",
       ">>>>>>> remote\n",
       "some other\n",
       "lines\n",
       "to align\n",
       ]]
    # Current case:
    _expected_partial = [[
        "<<<<<<< local\n",
        "local\nsome other\nlines\nto align\n",
        "||||||| base\n",
        "base\nsome other\nlines\nto align\n",
        "=======\n",
        "remote\nsome other\nlines\nto align\n",
        ">>>>>>> remote"]]
    expected_conflicts = [{
        "common_path": ("cells", 0),
        "local_diff": [
            op_patch('source', [
                op_addrange(
                    0, local[0][0:1]),
                op_removerange(0, 1)],
            )],
        "remote_diff": [
            op_patch('source', [
                op_addrange(
                    0, remote[0][0:1]),
                op_removerange(0, 1)],

            )],
        }]
    merge_args = copy.deepcopy(args)
    merge_args.merge_strategy = "use-base"
    merge_args.input_strategy = "inline"
    _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
Ejemplo n.º 18
0
def test_pop_patch_unpoppable():
    md = MergeDecision(common_path=("a", "b"),
                       action="base",
                       conflict=True,
                       local_diff=[op_remove("c")],
                       remote_diff=[op_patch("c", [op_remove("d")])])
    dec = pop_patch_decision(md)
    assert dec is None
Ejemplo n.º 19
0
def test_patch_dict():
    # Test +, single item insertion
    assert patch({}, [op_add("d", 4)]) == {"d": 4}
    assert patch({"a": 1}, [op_add("d", 4)]) == {"a": 1, "d": 4}

    #assert patch({"d": 1}, [op_add("d", 4)]) == {"d": 4} # currently triggers assert, raise exception or allow?

    # Test -, single item deletion
    assert patch({"a": 1}, [op_remove("a")]) == {}
    assert patch({"a": 1, "b": 2}, [op_remove("a")]) == {"b": 2}

    # Test :, single item replace
    assert patch({"a": 1, "b": 2}, [op_replace("a", 3)]) == {"a": 3, "b": 2}
    assert patch({"a": 1, "b": 2}, [op_replace("a", 3), op_replace("b", 5)]) == {"a": 3, "b": 5}

    # Test !, item patch
    subdiff = [op_patch(0, [op_patch(0, [op_replace(0, "H")])]), op_patch(1, [op_patch(0, [op_remove(0), op_add(0, "W")])])]
    assert patch({"a": ["hello", "world"], "b": 3}, [op_patch("a", subdiff)]) == {"a": ["Hello", "World"], "b": 3}
Ejemplo n.º 20
0
def test_merge_insert_cells_around_conflicting_cell():
    # Modifying an original cell and inserting a new cell on both sides
    source = [
        "def foo(x, y):",
        "    z = x * y",
        "    return z",
        ]
    local  = [["new local cell"],
              source + ["local"]]
    base   = [source]
    remote = [source + ["remote"],
              ["new remote cell"]]
    if 0:
        # This is how it would look if neither source or cell inserts resulted in conflicts:
        expected_partial = [["new local cell"],
                            source + ["local", "remote"],
                            ["new remote cell"]]
        expected_lco = []
        expected_rco = []
    elif 0:
        # This is how it would look if source inserts but not cell inserts resulted in conflicts:
        expected_partial = [local[0], source, remote[1]]
        expected_lco = _patch_cell_source(1, [op_addrange(len("\n".join(source)), "\nlocal")])
        expected_rco = _patch_cell_source(1, [op_addrange(len("\n".join(source)), "\nremote")])
    else:
        # In current behaviour:
        # - base cell 0 is aligned correctly (this is the notebook diff heuristics)
        # - conflicting edits of base cell 0 detected
        # - local insert before cell 0 is treated as part of conflict on base cell 0
        # - remote insert after cell 0 is not treated as part of conflict
        # FIXME: This may not be the exact behaviour we want:
        # - For source, we might want both inserts to be part of the conflict.
        #   (if so, fix in generic merge and chunk collection)
        # - For cells, we might want both inserts to be ok, they are separate new cells after all. (use autoresolve for this?)
        # - Figure out the best behaviour and make it happen!
        expected_partial = [source, remote[1]]
        expected_lco = [op_patch("cells", [
            op_addrange(0, [nbformat.v4.new_code_cell(source=["new local cell"])]),
            op_patch(0, [op_patch("source",
                                  [op_addrange(len("\n".join(source)), "\nlocal")]
                                  )]),
            ])]
        expected_rco = _patch_cell_source(0, [op_addrange(len("\n".join(source)), "\nremote")])
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)
Ejemplo n.º 21
0
def test_pop_patch_unpoppable():
    md = MergeDecision(
        common_path=("a", "b"),
        action="base",
        conflict=True,
        local_diff=[op_remove("c")],
        remote_diff=[op_patch("c", [op_remove("d")])]
    )
    dec = pop_patch_decision(md)
    assert dec is None
Ejemplo n.º 22
0
def test_autoresolve_inline_source():
    value = """\
def hello():
    print("world!")
"""
    le = op_patch("source", [op_replace(24, 'W')])  # FIXME: Character based here, should be linebased?
    re = op_patch("source", [op_replace(29, '.')])
    expected = """\
<<<<<<< local
def hello():
    print("World!")
======= base
def hello():
    print("world!")
======= remote
def hello():
    print("world.")
>>>>>>>
"""
    actual = make_inline_source_value(value, le, re)
    print(actual)
    assert actual == expected
Ejemplo n.º 23
0
def test_ensure_common_path_single_level():
    diffs = [[op_patch("c", [op_remove("e")])],
             [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_remove("e")], [op_remove("d")]])
Ejemplo n.º 24
0
def test_ensure_common_path_no_change():
    diffs = [[op_remove("c")], [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b"), diffs)
Ejemplo n.º 25
0
def test_merge_input_strategy_inline_source_conflict():
    # Conflicting cell inserts at same location as removing old cell
    local = [["local\n", "some other\n", "lines\n", "to align\n"]]
    base = [["base\n", "some other\n", "lines\n", "to align\n"]]
    remote = [["remote\n", "some other\n", "lines\n", "to align\n"]]
    # Ideal case:
    if have_git:
        expected_partial = [[
            "<<<<<<< local\n",
            "local\n",
            "=======\n",
            "remote\n",
            ">>>>>>> remote\n",
            "some other\n",
            "lines\n",
            "to align\n",
            ]]
    else:
        # Fallback is not very smart yet:
        expected_partial = [[
            "<<<<<<< local\n",
            "local\n",
            "some other\n",
            "lines\n",
            "to align\n",
            #'||||||| base\n',
            #'base\n',
            #'some other\n',
            #'lines\n',
            #'to align\n',
            "=======\n",
            "remote\n",
            "some other\n",
            "lines\n",
            "to align\n",
            ">>>>>>> remote",
            ]]
    expected_conflicts = [{
        "common_path": ("cells", 0, "source"),
        "local_diff": [
            op_addrange(0, local[0][0:1]),
            op_removerange(0, 1)
            ],
        "remote_diff": [
            op_addrange(0, remote[0][0:1]),
            op_removerange(0, 1)
            ],
        "custom_diff": [
            op_addrange(0, expected_partial[0]),
            op_removerange(0, len(base[0]))
            ],
        }]
    expected_conflicts = [{
        "common_path": ("cells", 0),
        "conflict": True,
        "action": "custom",
        "local_diff": [op_patch("source", [
            op_addrange(0, local[0][0:1]),
            op_removerange(0, 1)
            ])],
        "remote_diff": [op_patch("source", [
            op_addrange(0, remote[0][0:1]),
            op_removerange(0, 1)
            ])],
        "custom_diff": [op_replace("source", "".join(expected_partial[0]))],
        }]
    merge_args = copy.deepcopy(args)
    merge_args.merge_strategy = "use-base"
    merge_args.input_strategy = "inline"
    _check_sources(base, local, remote, expected_partial, expected_conflicts, merge_args)
Ejemplo n.º 26
0
def test_merge_simple_cell_sources():
    # A very basic test: Just checking changes to a single cell source,

    # No change
    local = [["same"]]
    base = [["same"]]
    remote = [["same"]]
    expected_partial = [["same"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # One sided change
    local = [["same"]]
    base = [["same"]]
    remote = [["different"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # One sided change
    local = [["different"]]
    base = [["same"]]
    remote = [["same"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Same change on both sides
    local = [["different"]]
    base = [["same"]]
    remote = [["different"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Conflicting cell inserts at same location as removing old cell
    local = [["local"]]
    base = [["base"]]
    remote = [["remote"]]
    expected_partial = [["base"]]
    expected_lco = [
        op_patch("cells", [
            op_addrange(
                0, [nbformat.v4.new_code_cell(source) for source in local]),
            op_removerange(0, 1)
        ])
    ]
    expected_rco = [
        op_patch("cells", [
            op_addrange(
                0, [nbformat.v4.new_code_cell(source) for source in remote]),
            op_removerange(0, 1)
        ])
    ]
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Cell inserts at same location but no other modifications: should this be accepted?
    local = [["base"], ["local"]]
    base = [["base"]]
    remote = [["base"], ["remote"]]
    if 0:  # Treat as conflict
        expected_partial = [["base"]]
        expected_lco = [
            op_patch("cells", [
                op_addrange(
                    1, [nbformat.v4.new_code_cell(source)
                        for source in local]),
            ])
        ]
        expected_rco = [
            op_patch("cells", [
                op_addrange(
                    1,
                    [nbformat.v4.new_code_cell(source) for source in remote]),
            ])
        ]
    else:  # Treat as non-conflict (insert both)
        expected_partial = [["base"], ["local"], ["remote"]]
        expected_lco = []
        expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)
Ejemplo n.º 27
0
def test_merge_conflicting_nested_dicts():
    # Note: Tests in here were written by writing up the last version
    # and then copy-pasting and deleting pieces to simplify...
    # Not pretty for production code but the explicitness is convenient when the tests fail.

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}}
    l = {"a": {"x": 2}}
    r = {"a": {"x": 3}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}}
    assert lc == [op_patch("a", [op_replace("x", 2)]),
                  ]
    assert rc == [op_patch("a", [op_replace("x", 3)]),
                  ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {}}
    l = {"a": {"y": 4}}
    r = {"a": {"y": 5}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {}}
    assert lc == [op_patch("a", [op_add("y", 4)]),
                  ]
    assert rc == [op_patch("a", [op_add("y", 5)]),
                  ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}}
    l = {"a": {"x": 2, "y": 4}}
    r = {"a": {"x": 3, "y": 5}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}}
    assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
                  ]
    assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
                  ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1},         "d": {"x": 4, "y": 5}}
    l = {"a": {"x": 2, "y": 4}, "d":         {"y": 6}}
    r = {"a": {"x": 3, "y": 5}, "d": {"x": 5},       }
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}}
    assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
                  op_patch("d", [op_remove("x"), op_replace("y", 6)]),
                  ]
    assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
                  op_patch("d", [op_replace("x", 5), op_remove("y")]),
                  ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1},         "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    l = {"a": {"x": 2, "y": 4}, "d":         {"y": 6}, "m": {"x": 17}}
    r = {"a": {"x": 3, "y": 5}, "d": {"x": 5},         "m": {"x": 27}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
                  op_patch("d", [op_remove("x"), op_replace("y", 6)]),
                  op_patch("m", [op_replace("x", 17)]),
                  ]
    assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
                  op_patch("d", [op_replace("x", 5), op_remove("y")]),
                  op_patch("m", [op_replace("x", 27)]),
                  ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1},         "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    l = {"a": {"x": 2, "y": 4}, "d":         {"y": 6}, "m": {"x": 17}, "n": {"q": 9}}
    r = {"a": {"x": 3, "y": 5}, "d": {"x": 5},         "m": {"x": 27}, "n": {"q": 19}}
    m, lc, rc = merge(b, l, r)
    # Note that "n":{} gets added to the merge result even though it's empty
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}, "n": {}}
    assert lc == [op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
                  op_patch("d", [op_remove("x"), op_replace("y", 6)]),
                  op_patch("m", [op_replace("x", 17)]),
                  op_patch("n", [op_add("q", 9)])
                  ]
    assert rc == [op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
                  op_patch("d", [op_replace("x", 5), op_remove("y")]),
                  op_patch("m", [op_replace("x", 27)]),
                  op_patch("n", [op_add("q", 19)])
                  ]
Ejemplo n.º 28
0
def test_ensure_common_path_one_sided_remote():
    diffs = [[op_patch("c", [op_remove("d")])],
             []]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_remove("d")], None])
Ejemplo n.º 29
0
def _patch_cell_source(cell_index, source_diff):
    "Convenience function to create the diff that patches only the source of a specific cell."
    return [op_patch("cells", [op_patch(cell_index, [op_patch("source", source_diff)])])]
Ejemplo n.º 30
0
def test_ensure_common_path_one_sided_remote():
    diffs = [[op_patch("c", [op_remove("d")])], []]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_remove("d")], None])
Ejemplo n.º 31
0
def test_patch_str():
    # Test +, single item insertion
    assert patch("42", [op_patch(0, [op_add(0, "3"), op_remove(1)])]) == "34"

    # Test -, single item deletion
    assert patch("3", [op_patch(0, [op_remove(0)])]) == ""
    assert patch("42", [op_patch(0, [op_remove(0)])]) == "2"
    assert patch("425", [op_patch(0, [op_remove(0)])]) == "25"
    assert patch("425", [op_patch(0, [op_remove(1)])]) == "45"
    assert patch("425", [op_patch(0, [op_remove(2)])]) == "42"

    # Test :, single item replace
    assert patch("52", [op_patch(0, [op_replace(0, "4")])]) == "42"
    assert patch("41", [op_patch(0, [op_replace(1, "2")])]) == "42"
    assert patch("42", [op_patch(0, [op_replace(0, "3"), op_replace(1, "5")])]) == "35"
    assert patch("hello", [op_patch(0, [op_replace(0, "H")])]) == "Hello"
    # Replace by delete-then-insert
    assert patch("world", [op_patch(0, [op_remove(0), op_add(0, "W")])]) == "World"

    # Test !, item patch (doesn't make sense for str)
    pass

    # Test ++, sequence insertion
    assert patch("", [op_patch(0, [op_addrange(0, "34"), op_add(0, "5"), op_addrange(0, "67")])]) == "34567"

    # Test --, sequence deletion
    assert patch("abcd", [op_patch(0, [op_removerange(0, 2)])]) == "cd"
    assert patch("abcd", [op_patch(0, [op_removerange(1, 2)])]) == "ad"
    assert patch("abcd", [op_patch(0, [op_removerange(2, 2)])]) == "ab"
Ejemplo n.º 32
0
def test_merge_input_strategy_inline_source_conflict():
    # Conflicting cell inserts at same location as removing old cell
    local = [["local\n", "some other\n", "lines\n", "to align\n"]]
    base = [["base\n", "some other\n", "lines\n", "to align\n"]]
    remote = [["remote\n", "some other\n", "lines\n", "to align\n"]]
    # Ideal case:
    if have_git:
        expected_partial = [[
            "<<<<<<< local\n",
            "local\n",
            "=======\n",
            "remote\n",
            ">>>>>>> remote\n",
            "some other\n",
            "lines\n",
            "to align\n",
        ]]
    else:
        # Fallback is not very smart yet:
        expected_partial = [[
            "<<<<<<< local\n",
            "local\n",
            "some other\n",
            "lines\n",
            "to align\n",
            #'||||||| base\n',
            #'base\n',
            #'some other\n',
            #'lines\n',
            #'to align\n',
            "=======\n",
            "remote\n",
            "some other\n",
            "lines\n",
            "to align\n",
            ">>>>>>> remote",
        ]]
    expected_conflicts = [{
        "common_path": ("cells", 0, "source"),
        "local_diff": [op_addrange(0, local[0][0:1]),
                       op_removerange(0, 1)],
        "remote_diff": [op_addrange(0, remote[0][0:1]),
                        op_removerange(0, 1)],
        "custom_diff":
        [op_addrange(0, expected_partial[0]),
         op_removerange(0, len(base[0]))],
    }]
    expected_conflicts = [{
        "common_path": ("cells", 0),
        "conflict":
        True,
        "action":
        "custom",
        "local_diff": [
            op_patch("source",
                     [op_addrange(0, local[0][0:1]),
                      op_removerange(0, 1)])
        ],
        "remote_diff": [
            op_patch("source",
                     [op_addrange(0, remote[0][0:1]),
                      op_removerange(0, 1)])
        ],
        "custom_diff": [op_replace("source", "".join(expected_partial[0]))],
    }]
    merge_args = copy.deepcopy(args)
    merge_args.merge_strategy = "use-base"
    merge_args.input_strategy = "inline"
    _check_sources(base, local, remote, expected_partial, expected_conflicts,
                   merge_args)
Ejemplo n.º 33
0
def test_ensure_common_path_multilevel_intermediate():
    diffs = [[op_patch("c", [op_patch("d", [op_remove("f")])])],
             [op_patch("c", [op_patch("e", [op_remove("g")])])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_patch("d", [op_remove("f")])],
                                     [op_patch("e", [op_remove("g")])]])
Ejemplo n.º 34
0
def test_merge_simple_cell_sources():
    # A very basic test: Just checking changes to a single cell source,

    # No change
    local  = [["same"]]
    base   = [["same"]]
    remote = [["same"]]
    expected_partial = [["same"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # One sided change
    local  = [["same"]]
    base   = [["same"]]
    remote = [["different"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # One sided change
    local  = [["different"]]
    base   = [["same"]]
    remote = [["same"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Same change on both sides
    local  = [["different"]]
    base   = [["same"]]
    remote = [["different"]]
    expected_partial = [["different"]]
    expected_lco = []
    expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Conflicting cell inserts at same location as removing old cell
    local  = [["local"]]
    base   = [["base"]]
    remote = [["remote"]]
    expected_partial = [["base"]]
    expected_lco = [op_patch("cells", [
        op_addrange(0, [nbformat.v4.new_code_cell(source) for source in local]),
        op_removerange(0, 1)])]
    expected_rco = [op_patch("cells", [
        op_addrange(0, [nbformat.v4.new_code_cell(source) for source in remote]),
        op_removerange(0, 1)])]
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)

    # Cell inserts at same location but no other modifications: should this be accepted?
    local  = [["base"], ["local"]]
    base   = [["base"]]
    remote = [["base"], ["remote"]]
    if 0:  # Treat as conflict
        expected_partial = [["base"]]
        expected_lco = [op_patch("cells", [
            op_addrange(1, [nbformat.v4.new_code_cell(source) for source in local]),
            ])]
        expected_rco = [op_patch("cells", [
            op_addrange(1, [nbformat.v4.new_code_cell(source) for source in remote]),
            ])]
    else:  # Treat as non-conflict (insert both)
        expected_partial = [["base"], ["local"], ["remote"]]
        expected_lco = []
        expected_rco = []
    _check(base, local, remote, expected_partial, expected_lco, expected_rco)
Ejemplo n.º 35
0
def test_ensure_common_path_one_sided_empty():
    diffs = [[],
             [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [None, [op_remove("d")]])
Ejemplo n.º 36
0
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
Ejemplo n.º 37
0
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)
Ejemplo n.º 38
0
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)
Ejemplo n.º 39
0
def test_merge_conflicting_nested_dicts():
    # Note: Tests in here were written by writing up the last version
    # and then copy-pasting and deleting pieces to simplify...
    # Not pretty for production code but the explicitness is convenient when the tests fail.

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}}
    l = {"a": {"x": 2}}
    r = {"a": {"x": 3}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}}
    assert lc == [
        op_patch("a", [op_replace("x", 2)]),
    ]
    assert rc == [
        op_patch("a", [op_replace("x", 3)]),
    ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {}}
    l = {"a": {"y": 4}}
    r = {"a": {"y": 5}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {}}
    assert lc == [
        op_patch("a", [op_add("y", 4)]),
    ]
    assert rc == [
        op_patch("a", [op_add("y", 5)]),
    ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}}
    l = {"a": {"x": 2, "y": 4}}
    r = {"a": {"x": 3, "y": 5}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}}
    assert lc == [
        op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
    ]
    assert rc == [
        op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
    ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}}
    l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}}
    r = {
        "a": {
            "x": 3,
            "y": 5
        },
        "d": {
            "x": 5
        },
    }
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}}
    assert lc == [
        op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
        op_patch("d", [op_remove("x"), op_replace("y", 6)]),
    ]
    assert rc == [
        op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
        op_patch("d", [op_replace("x", 5), op_remove("y")]),
    ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}}
    r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}}
    m, lc, rc = merge(b, l, r)
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    assert lc == [
        op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
        op_patch("d", [op_remove("x"), op_replace("y", 6)]),
        op_patch("m", [op_replace("x", 17)]),
    ]
    assert rc == [
        op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
        op_patch("d", [op_replace("x", 5), op_remove("y")]),
        op_patch("m", [op_replace("x", 27)]),
    ]

    # local and remote each adds, deletes, and modifies entries inside nested structure with everything conflicting
    b = {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}}
    l = {"a": {"x": 2, "y": 4}, "d": {"y": 6}, "m": {"x": 17}, "n": {"q": 9}}
    r = {"a": {"x": 3, "y": 5}, "d": {"x": 5}, "m": {"x": 27}, "n": {"q": 19}}
    m, lc, rc = merge(b, l, r)
    # Note that "n":{} gets added to the merge result even though it's empty
    assert m == {"a": {"x": 1}, "d": {"x": 4, "y": 5}, "m": {"x": 7}, "n": {}}
    assert lc == [
        op_patch("a", [op_replace("x", 2), op_add("y", 4)]),
        op_patch("d", [op_remove("x"), op_replace("y", 6)]),
        op_patch("m", [op_replace("x", 17)]),
        op_patch("n", [op_add("q", 9)])
    ]
    assert rc == [
        op_patch("a", [op_replace("x", 3), op_add("y", 5)]),
        op_patch("d", [op_replace("x", 5), op_remove("y")]),
        op_patch("m", [op_replace("x", 27)]),
        op_patch("n", [op_add("q", 19)])
    ]
Ejemplo n.º 40
0
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
Ejemplo n.º 41
0
def test_ensure_common_path_no_change():
    diffs = [[op_remove("c")], [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b"), diffs)
Ejemplo n.º 42
0
def _patch_cell_source(cell_index, source_diff):
    "Convenience function to create the diff that patches only the source of a specific cell."
    return [
        op_patch("cells",
                 [op_patch(cell_index, [op_patch("source", source_diff)])])
    ]
Ejemplo n.º 43
0
def test_patch_str():
    # Test +, single item insertion
    assert patch("42", [op_patch(0, [op_add(0, "3"), op_remove(1)])]) == "34"

    # Test -, single item deletion
    assert patch("3", [op_patch(0, [op_remove(0)])]) == ""
    assert patch("42", [op_patch(0, [op_remove(0)])]) == "2"
    assert patch("425", [op_patch(0, [op_remove(0)])]) == "25"
    assert patch("425", [op_patch(0, [op_remove(1)])]) == "45"
    assert patch("425", [op_patch(0, [op_remove(2)])]) == "42"

    # Test :, single item replace
    assert patch("52", [op_patch(0, [op_replace(0, "4")])]) == "42"
    assert patch("41", [op_patch(0, [op_replace(1, "2")])]) == "42"
    assert patch("42", [op_patch(
        0, [op_replace(0, "3"), op_replace(1, "5")])]) == "35"
    assert patch("hello", [op_patch(0, [op_replace(0, "H")])]) == "Hello"
    # Replace by delete-then-insert
    assert patch("world",
                 [op_patch(0, [op_remove(0), op_add(0, "W")])]) == "World"

    # Test !, item patch (doesn't make sense for str)
    pass

    # Test ++, sequence insertion
    assert patch("", [
        op_patch(0,
                 [op_addrange(0, "34"),
                  op_add(0, "5"),
                  op_addrange(0, "67")])
    ]) == "34567"

    # Test --, sequence deletion
    assert patch("abcd", [op_patch(0, [op_removerange(0, 2)])]) == "cd"
    assert patch("abcd", [op_patch(0, [op_removerange(1, 2)])]) == "ad"
    assert patch("abcd", [op_patch(0, [op_removerange(2, 2)])]) == "ab"
Ejemplo n.º 44
0
def test_ensure_common_path_single_level():
    diffs = [[op_patch("c", [op_remove("e")])],
             [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_remove("e")], [op_remove("d")]])
Ejemplo n.º 45
0
def test_ensure_common_path_multilevel_intermediate():
    diffs = [[op_patch("c", [op_patch("d", [op_remove("f")])])],
             [op_patch("c", [op_patch("e", [op_remove("g")])])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [[op_patch("d", [op_remove("f")])],
                                     [op_patch("e", [op_remove("g")])]])
Ejemplo n.º 46
0
def test_ensure_common_path_one_sided_empty():
    diffs = [[], [op_patch("c", [op_remove("d")])]]
    res = ensure_common_path(("a", "b"), diffs)
    assert res == (("a", "b", "c"), [None, [op_remove("d")]])
Ejemplo n.º 47
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)]),
    ]
Ejemplo n.º 48
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)
            ]),
        ]