def test_sd_si(self):
        op1 = Op('sd', [], offset=3, val=3)
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # delete range has greater index than this insert. Do nothing
        op2 = Op('si', [], offset=2, val="ABC")
        op2.ot(cs1)
        assert op2.t_offset == 2
        assert op2.t_val == "ABC"
        op1.remove_old_hazards(purge=True)

        # edge case. avoid deleting
        op3 = Op('si', [], offset=3, val="ABC")
        op3.ot(cs1)
        assert op3.t_offset == 3
        assert op3.t_val == "ABC"
        op1.remove_old_hazards(purge=True)

        # text was put into delete range, so get rid of it.
        op4 = Op('si', [], offset=4, val="ABC")
        op4.ot(cs1)
        assert op4.t_offset == 3
        assert op4.t_val == ""
        op1.remove_old_hazards(purge=True)

        # text is at edge after delete range
        op5 = Op('si', [], offset=6, val="ABC")
        op5.ot(cs1)
        assert op5.t_offset == 3
        assert op5.t_val == "ABC"
    def test_si_sd(self):
        op1 = Op('si', [], offset=3, val="ABC")
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # insertion was at a later index than this delete. No change
        op2 = Op('sd', [], offset=0, val=3)
        op2.ot(cs1)
        assert op2.t_offset == 0
        assert op2.t_val == 3
        op1.remove_old_hazards(purge=True)

        # this deletion should expand to delete inserted text as well.
        op3 = Op('sd', [], offset=2, val=2)
        op3.ot(cs1)
        assert op3.t_offset == 2
        assert op3.t_val == 5
        op1.remove_old_hazards(purge=True)

        # edge case, don't delete text if don't have have to. Shift
        # delete range.
        op4 = Op('sd', [], offset=3, val=2)
        op4.ot(cs1)
        assert op4.t_offset == 6
        assert op4.t_val == 2
        op1.remove_old_hazards(purge=True)

        # insertion was at lower index. shift delete range forward.
        op5 = Op('sd', [], offset=4, val=2)
        op5.ot(cs1)
        assert op5.t_offset == 7
        assert op5.t_val == 2
    def test_oi_od(self):
        """
        A past opperation is an object insert which gets applied to these
        object deletes.
        """
        array_path = ['a', 'b', 3, 4]
        op1 = Op('oi', array_path, offset='X', val=['X', 'Y'])
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op('od', ['a', 'b', 3, 5], offset='R')
        op2.ot(cs1)
        assert op2.t_path == ['a', 'b', 3, 5]
        assert op2.t_offset == 'R'
        assert not op2.is_noop()
        op1.hazards = []

        # op3 deletes a key were unknown op had inserted one. Could not have
        # intended to delete what it did not know, so noop
        op3 = Op('od', array_path, offset='X')
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 'X'
        assert op3.is_noop()
        op1.hazards = []

        # same as above, but well within the inserted value
        op4_path = array_path + ['X']
        op4 = Op('od', op4_path, offset='Y')
        op4.ot(cs1)
        assert op4.t_path == array_path + ['X']
        assert op4.t_offset == 'Y'
        assert op4.is_noop()
        op1.hazards = []

        # op5 is at same path, differant offset. No change
        op5 = Op('od', array_path, offset='R')
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_offset == 'R'
        assert not op5.is_noop()
        op1.hazards = []

        # op6 is at shorter path. No change
        op6 = Op('od', ['a'], offset='c')
        op6.ot(cs1)
        assert op6.t_path == ['a']
        assert op6.t_offset == 'c'
        assert not op6.is_noop()
        op1.hazards = []

        # op7 deletes whatever was previously changed
        op7 = Op('od', ['a'], offset='b')
        op7.ot(cs1)
        assert op7.t_path == ['a']
        assert op7.t_offset == 'b'
        assert not op7.is_noop()
        op1.hazards = []
    def test_ai_ai(self):
        """
        A past opperations is an array insert which gets applied to this op.
        """
        array_path = ["a", "b", 3, 4]
        op1 = Op("ai", array_path, offset=3, val=["X", "Y"])
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op("ai", ["a", "b", 3, 5], offset=2, val=["XYZ"])
        op2.ot(cs1)
        assert op2.t_path == ["a", "b", 3, 5]
        op1.remove_old_hazards(purge=True)

        # op3 happens at same path, but lower offset, no change
        op3 = Op("ai", array_path, offset=2, val=["XYZ"])
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 2
        op1.remove_old_hazards(purge=True)

        # op4 is at same path with offset to get pushed forward (edge case)
        op4 = Op("ai", array_path, offset=3, val=["XYZ"])
        op4.ot(cs1)
        assert op4.t_path == array_path
        assert op4.t_offset == 5
        op1.remove_old_hazards(purge=True)

        # op5 is at same path with offset to get pushed forward (not edge case)
        op5 = Op("ai", array_path, offset=8, val=["XYZ"])
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_offset == 10
        op1.remove_old_hazards(purge=True)

        # op6 path is in an array element being pushed forward (edge case)
        op6_path = array_path + [3, 9, "c"]
        op6 = Op("ai", op6_path, offset=8, val=["XYZ"])
        op6.ot(cs1)
        assert op6.t_path == array_path + [5, 9, "c"]
        assert op6.t_offset == 8
        op1.remove_old_hazards(purge=True)

        # op7 path is in an array element being pushed forward (not edge case)
        op7_path = array_path + [5, 9, "c"]
        op7 = Op("ai", op7_path, offset=8, val=["XYZ"])
        op7.ot(cs1)
        assert op7.t_path == array_path + [7, 9, "c"]
        op1.remove_old_hazards(purge=True)

        # op8 path is shorter then array's path, so no change
        op8_path = ["a", "b", 3]
        op8 = Op("ai", op8_path, offset=8, val=["XYZ"])
        op8.ot(cs1)
        assert op8.t_path == op8_path
        assert op8.t_offset == 8
        op1.remove_old_hazards(purge=True)
    def test_sd_sd(self):
        op1 = Op('sd', [], offset=3, val=3)
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op1 deletes a range after op2, so should not affect it
        #                |-- op1 --|
        # |-- op2 --|
        op2 = Op('sd', [], offset=1, val=2)
        op2.ot(cs1)
        assert op2.t_offset == 1
        assert op2.t_val == 2
        op1.remove_old_hazards(purge=True)

        # The end of op3 overlaps the start of op 1
        #          |-- op1 --|
        #   |-- op3 --|
        op3 = Op('sd', [], offset=2, val=2)
        op3.ot(cs1)
        assert op3.t_offset == 2
        assert op3.t_val == 1
        op1.remove_old_hazards(purge=True)

        # op1 range is encompased by op 4 range
        #     |-- op1 --|
        #   |---- op4 ----|
        op4 = Op('sd', [], offset=2, val=6)
        op4.ot(cs1)
        assert op4.t_offset == 2
        assert op4.t_val == 3
        op1.remove_old_hazards(purge=True)

        # op5 range is encompased by op1 range
        #   |---- op1 ----|
        #     |-- op5 --|
        op5 = Op('sd', [], offset=4, val=1)
        op5.ot(cs1)
        assert op5.t_offset == 3
        assert op5.t_val == 0
        op1.remove_old_hazards(purge=True)

        # start of op6 range overlaps end of op1 range
        #   |-- op1 --|
        #         |-- op6 --|
        op6 = Op('sd', [], offset=5, val=3)
        op6.ot(cs1)
        assert op6.t_offset == 3
        assert op6.t_val == 2
        op1.remove_old_hazards(purge=True)

        # start of op7 range is after start of op1 range
        #   |-- op1 --|
        #                |-- op7 --|
        op7 = Op('sd', [], offset=8, val=3)
        op7.ot(cs1)
        assert op7.t_offset == 5
        assert op7.t_val == 3
    def test_od_od(self):
        """
        A past opperation is an object delete which gets applied to these
        object deletes.
        """
        array_path = ['a', 'b', 3, 4]
        op1 = Op('od', array_path, offset='X')
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op('od', ['a', 'b', 3, 5], offset='Y')
        op2.ot(cs1)
        assert op2.t_path == ['a', 'b', 3, 5]
        assert op2.t_offset == 'Y'
        assert not op2.is_noop()
        op1.hazards = []

        # op3 happens at same path but different offset
        op3 = Op('od', array_path, offset='R')
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 'R'
        assert not op3.is_noop()
        op1.hazards = []

        # op4 tries to delete the same key as op1
        op4 = Op('od', array_path, offset='X')
        op4.ot(cs1)
        assert op4.is_noop()
        op1.hazards = []

        # op5 tries to delete something within what was already deleted
        op5_path = array_path + ['X']
        op5 = Op('od', op5_path, offset='R')
        op5.ot(cs1)
        assert op5.is_noop()
        op1.hazards = []

        # op6 is at a shorter, different path. No change
        op6_path = ['a']
        op6 = Op('od', op6_path, offset='c')
        op6.ot(cs1)
        assert op6.t_path == ['a']
        assert op6.t_offset == 'c'
        assert not op6.is_noop()
        op1.hazards = []

        # op9 is at shorter, same path. No change
        op9_path = ['a']
        op9 = Op('od', op9_path, offset='b')
        op9.ot(cs1)
        assert op9.t_path == op9_path
        assert op9.t_offset == 'b'
        assert not op9.is_noop()
        op1.hazards = []
    def test_linear(self):
        doc = Document(snapshot='')
        doc.HAS_EVENT_LOOP = False
        root = doc.get_root_changeset()
        cs0 = Changeset(doc.get_id(), "dummyuser", [root])
        doc.add_to_known_changesets(cs0)
        doc.insert_changeset_into_ordered_list(cs0)
        doc.update_unaccounted_changesets(cs0)

        assert root.get_unaccounted_changesets() == []
        assert cs0.get_unaccounted_changesets() == []

        
        cs1 = Changeset(doc.get_id(), "user1", [cs0])
        doc.add_to_known_changesets(cs1)
        doc.insert_changeset_into_ordered_list(cs1)
        doc.update_unaccounted_changesets(cs1)

        assert root.get_unaccounted_changesets() == []
        assert cs0.get_unaccounted_changesets() == []
        assert cs1.get_unaccounted_changesets() == []

        cs2 = Changeset(doc.get_id(), "user1", [cs1])
        doc.add_to_known_changesets(cs2)
        doc.insert_changeset_into_ordered_list(cs2)
        doc.update_unaccounted_changesets(cs2)

        assert root.get_unaccounted_changesets() == []
        assert cs0.get_unaccounted_changesets() == []
        assert cs1.get_unaccounted_changesets() == []
        assert cs2.get_unaccounted_changesets() == []
 def test_get_dependency_ids(self):
     cs0 = Changeset('doc_id', 'user_id', [])
     assert cs0.get_dependency_ids() == []
     
     cs1 = Changeset('doc_id', 'user_id', ['randomid'])
     assert cs1.get_dependency_ids() == ['randomid']
     
     cs2 = Changeset('doc_id', 'user_id', [cs1])
     assert cs2.get_dependency_ids() == [cs1.get_id()]
     
     cs3 = Changeset('doc_id', 'user_id', [cs2, 'otherrandomid'])
     assert cs3.get_dependency_ids() == [cs2.get_id(), 'otherrandomid']
    def test_od_oi(self):
        """
        A past opperations is an object delete which gets applied to this
        object insert.
        """
        array_path = ['a', 'b', 3, 4]
        op1 = Op('od', array_path, offset='X')
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op('oi', ['a', 'b', 3, 5], offset='Y', val=['XYZ'])
        op2.ot(cs1)
        assert op2.t_path == ['a', 'b', 3, 5]
        assert op2.t_offset == 'Y'
        assert op2.t_val == ['XYZ']
        assert not op2.is_noop()
        op1.hazards = []

        # op3 happens at path, but differant offset, no change
        op3 = Op('oi', array_path, offset='W', val=['XYZ'])
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 'W'
        assert not op3.is_noop()
        op1.hazards = []

        # op4 happens within deletion
        op4_path = array_path + ['X']
        op4 = Op('oi', op4_path, offset='R', val=['XYZ'])
        op4.ot(cs1)
        assert op4.is_noop()
        op1.hazards = []

        # op5 inserts right where there was a deletion. Some previous conflict,
        # so noop
        op5 = Op('oi', array_path, offset='X', val='XYZ')
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_offset == 'X'
        assert op5.t_val == 'XYZ'
        assert op5.is_noop()
        op1.hazards = []

        # op6 is at a partial path along deletion, no change
        op6_path = ['a']
        op6 = Op('oi', op6_path, offset='c', val=['XYZ'])
        op6.ot(cs1)
        assert op6.t_path == ['a']
        assert op6.t_offset == 'c'
        assert op6.t_val == ['XYZ']
        assert not op6.is_noop()
        op1.hazards = []
    def test_has_needed_dependencies(self):
        doc = self.doc0

        cs1 = Changeset(doc.get_id(), 'user', [doc.get_root_changeset()])
        assert doc.has_needed_dependencies(cs1)

        cs2 = Changeset(doc.get_id(), 'user', [cs1])
        assert not doc.has_needed_dependencies(cs2)

        doc.receive_changeset(cs1)
        assert doc.has_needed_dependencies(cs2)

        cs3 = Changeset(doc.get_id(), 'user', [cs1, cs2])
        assert not doc.has_needed_dependencies(cs3)

        doc.receive_changeset(cs2)
        assert doc.has_needed_dependencies(cs3)

        cs4 = Changeset(doc.get_id(), 'user', [cs3, "555"])
        assert not doc.has_needed_dependencies(cs4)

        doc.receive_changeset(cs3)
        assert not doc.has_needed_dependencies(cs4)

        cs5 = Changeset(doc.get_id(), 'user', [cs1])
        cs5.set_id("555")
        doc.receive_changeset(cs5)
        cs4.relink_changesets(doc.all_known_changesets)
        assert cs5 in cs4.get_parents()
        assert cs4.has_full_dependency_info()
        assert doc.has_needed_dependencies(cs4)
    def test_oi_oi(self):
        """
        A past opperation is an Object insert which gets applied to this object
        insert. The only opportunity for conflict is if the later insert
        happens along a path that is no longer valid.
        """
        array_path = ['a', 'b', 3, 4]
        op1 = Op('oi', array_path, offset='X', val=['Y', 'Z'])
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op('oi', ['a', 'b', 3, 5], offset='c', val=["XYZ"])
        op2.ot(cs1)
        assert op2.t_path == ['a', 'b', 3, 5]
        op1.hazards = []

        # op3 happens along path, so it will just overwrite past data
        op3 = Op('oi', ['a'], offset='b', val="XYZ")
        op3.ot(cs1)
        assert op3.t_path == ['a']
        assert op3.t_offset == 'b'
        assert op3.t_val == 'XYZ'
        op1.hazards = []

        # op4 is at same path, different offset, so no conflict
        op4 = Op('oi', array_path, offset='W', val="WWW")
        op4.ot(cs1)
        assert op4.t_path == array_path
        assert op4.t_offset == 'W'
        assert op4.t_val == 'WWW'
        assert not op4.is_noop()
        op1.hazards = []

        # op5 is at same path and offset, so previous op takes precedence
        op5 = Op('oi', array_path, offset='X', val=["XXX"])
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_offset == 'X'
        assert op5.t_val == ['XXX']
        assert op5.is_noop()
        op1.hazards = []

        # op6 path is deep within a previous object insert
        op6_path = array_path + ['X', 9, 'c']
        op6 = Op('oi', op6_path, offset=8, val=["XYZ"])
        op6.ot(cs1)
        assert op6.is_noop()
        op1.hazards = []
Ejemplo n.º 12
0
def build_changesets_from_tuples(css_data, doc):
    """
    When testing its easiest to write the desired changesets as tuples, then
    this will convert that list of tuples into a list of changesets for the
    desired doc.
    """
    css = []
    for cs_data in css_data:
        action, offset, val, deps, _id = cs_data
        deps = [doc.get_root_changeset() if dep == 'root' else dep
                for dep in deps]
        cs = Changeset(doc.get_id(), 'u1', deps)
        cs.add_op(Op(action, [], offset=offset, val=val))
        cs.set_id(_id)
        css.append(cs)
    return css
Ejemplo n.º 13
0
    def test_set_root_preceding(self):
        """
        A preceding operation did a set at the root. Any following
        operations should be transformed to a noop.
        """
        op1 = Op("set", [], val="ABC")
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        # op2 happens at root so should be affected
        op2 = Op("si", [], offset=2, val="XYZ")
        op2.ot(cs1)
        assert op2.is_noop() == True

        # op3 happens not at root. still should be affected
        op3 = Op("si", ["k", 3, "j"], offset=3, val="XYZ")
        op3.ot(cs1)
        assert op3.is_noop() == True
Ejemplo n.º 14
0
    def test_set_root_after(self):
        """
        The set root occurs after other unknown operations. They should
        have no effect.
        """
        op1 = Op("si", [], offset=0, val="ABC")
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        op2 = Op("set", [], val="XYZ")
        op2.ot(cs1)
        assert op2.is_noop() == False
        assert op1.is_noop() == False

        # op3 happens not at root. still should be affected
        op3 = Op("set", ["k", 3, "j"], offset=3, val="XYZ")
        op3.ot(cs1)
        assert op3.is_noop() == False
Ejemplo n.º 15
0
    def test_complex_path(self):
        op1 = Op("set", ["k", 3, "j"], val="ABC")
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        op2 = Op("si", [], val="XYZ")
        op2.ot(cs1)
        assert op2.is_noop() == False
        assert op1.is_noop() == False

        op3 = Op("si", ["k", 3, "j"], offset=3, val="XYZ")
        op3.ot(cs1)
        assert op3.is_noop() == True
        assert op1.is_noop() == False

        op4 = Op("si", ["k", 3, "j", "h"], offset=3, val="XYZ")
        op4.ot(cs1)
        assert op4.is_noop() == True
        assert op1.is_noop() == False
    def test_si_si(self):
        op1 = Op('si', [], offset=3, val="ABC")
        cs1 = Changeset('doc_id', 'author', [])
        cs1.add_op(op1)

        # op2 happens at a lower offset, so should not be affected
        op2 = Op('si', [], offset=2, val="XYZ")
        op2.ot(cs1)
        assert op2.t_offset == 2
        op1.remove_old_hazards(purge=True)

        # op3 happens at an equal offset, so should be pushed forward
        op2 = Op('si', [], offset=3, val="XYZ")
        op2.ot(cs1)
        assert op2.t_offset == 6
        op1.remove_old_hazards(purge=True)

        # op4 happens at a later offset, so should be pushed forward
        op2 = Op('si', [], offset=5, val="XYZ")
        op2.ot(cs1)
        assert op2.t_offset == 8
    def build_branch(self):
        doc = self.doc
        # pick a random changeset to start building off of
        parent = random.choice(doc.get_ordered_changesets())
        branch_length = random.choice(xrange(4, 15))
        for x in xrange(branch_length):
            # revert document to when it went as far as parent
            doc.rebuild_historical_document([parent])
            cs = Changeset(doc.get_id(), str(self.user_vector), [parent])
            op = self.build_random_insert_or_delete()
            cs.add_op(op)
            doc.receive_changeset(cs)
            parent = cs
            if len(self.remaining_chars) == 0:
                break
        # Pull back in all changesets
        doc.pull_from_pending_list()

        # create a new changeset which just ties that branch back to the other
        # dependencies
        deps = doc.get_dependencies()
        deps.remove(parent)
        # chose another dep, if able
        if len(deps) != 0:
            dep = random.choice(deps)
            deps = [parent, dep]
            cs = Changeset(doc.get_id(), str(self.user_vector), deps)
            self.user_vector += 1
            op = self.build_random_insert_or_delete()
            cs.add_op(op)
            doc.receive_changeset(cs)
    def setup_method(self, method):
        doc = Document(snapshot='0123')
        doc.HAS_EVENT_LOOP = False
        self.doc = doc
        root = doc.get_root_changeset()

        #
        A0 = Changeset(doc.get_id(), 'u1', [root])
        A0.add_op(Op('si', [], offset=1, val='ab'))
        self.A0 = A0

        B0 = Changeset(doc.get_id(), 'u2', [root])
        B0.add_op(Op('si', [], offset=1, val='cd'))
        self.B0 = B0

        A1 = Changeset(doc.get_id(), 'u1', [A0])
        A1.add_op(Op('sd', [], offset=2, val=2))
        self.A1 = A1
 def build_branch(self):
     doc = self.doc
     parent = doc.get_root_changeset()
     letters_to_insert = len(self.remaining_chars)
     first_branch = False
     if len(doc.get_ordered_changesets()) == 1:
         letters_to_insert = int(random.triangular(0, self.MAX_CHARS))
         first_branch = True
     else:
         doc.rebuild_historical_document([parent])
     while letters_to_insert > 0:
         cs = Changeset(doc.get_id(), str(self.user_vector), [parent])
         op = self.build_random_insert_or_delete()
         cs.add_op(op)
         doc.receive_changeset(cs, pull=False)
         if op.is_string_insert():
             letters_to_insert -= len(op.get_val())
         parent = cs
         if len(self.remaining_chars) == 0:
             break
     # Pull back in all changesets
     if not first_branch:
         doc.pull_from_pending_list()
    def test_delete_in_second_branch(self):
        """
        """
        doc = Document(snapshot='abcdefghij')
        doc.HAS_EVENT_LOOP = False
        root = doc.get_root_changeset()

        # construct branch A, which begins with a string delete
        A0 = Changeset(doc.get_id(), 'u1', [root])
        A0.add_op(Op('sd', [], offset=5, val=5))
        A0.set_id('2A')
        doc.receive_changeset(A0)
        assert doc.get_snapshot() == 'abcde'

        # Branch B has common parent with A. It inserts strings at the end
        # which should not be deleted.
        B0 = Changeset(doc.get_id(), 'u2', [root])
        for i in xrange(10):
            op = Op('si', [], offset=10 + i, val=str(i))
            B0.add_op(op)
        B0.set_id('1B')
        doc.receive_changeset(B0)
        assert doc.get_snapshot() == 'abcde0123456789'
    def setup_method(self, method):
        doc = Document(snapshot='0123')
        doc.HAS_EVENT_LOOP = False
        self.doc = doc
        root = doc.get_root_changeset()

        #
        A0 = Changeset(doc.get_id(), 'u1', [root])
        A0.add_op(Op('si', [], offset=1, val='ab'))
        self.A0 = A0

        B0 = Changeset(doc.get_id(), 'u2', [root])
        B0.add_op(Op('si', [], offset=1, val='cd'))
        self.B0 = B0

        # instead of deleting range, A will delete character one at a time.
        A1 = Changeset(doc.get_id(), 'u1', [A0])
        A1.add_op(Op('sd', [], offset=2, val=1))
        A1.add_op(Op('sd', [], offset=2, val=1))
        self.A1 = A1
Ejemplo n.º 22
0
    def test_sequence_of_set_ops(self):
        op1 = Op("set", ["k", 3, "j"], val="ABC")
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        op2 = Op("set", ["k", 3, "j"], val="XYZ")
        op2.ot(cs1)
        assert op2.is_noop() == True
        assert op1.is_noop() == False

        cs2 = Changeset("doc_id", "author", [cs1])
        cs2.add_op(op2)

        op3 = Op("set", ["k", 3, "j"], val="XYZ")
        op3.ot(cs2)
        assert op2.is_noop() == True
        assert op3.is_noop() == False
    def test_ai_ad(self):
        """
        A past opperation is an array insert which gets applied to these array
        deletes.
        """
        array_path = ["a", "b", 3, 4]
        op1 = Op("ai", array_path, offset=3, val=["X", "Y"])
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op("ad", ["a", "b", 3, 5], offset=2, val=3)
        op2.ot(cs1)
        assert op2.t_path == ["a", "b", 3, 5]
        assert op2.t_offset == 2
        assert op2.t_val == 3
        op1.remove_old_hazards(purge=True)

        # op3 happens at same path, but past insert is before delete range, so
        # delete moves. (edge case)
        op3 = Op("ad", array_path, offset=3, val=3)
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 5
        op1.remove_old_hazards(purge=True)

        # op4 is at same path and will expand delete range to include past
        # op. (edge case)
        op4 = Op("ad", array_path, offset=2, val=3)
        op4.ot(cs1)
        assert op4.t_path == array_path
        assert op4.t_val == 5
        assert op4.t_offset == 2
        op1.remove_old_hazards(purge=True)

        # op5 is at same path and will expand delete range to include past
        # op. (other edge case)
        op5 = Op("ad", array_path, offset=1, val=3)
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_val == 5
        assert op5.t_offset == 1
        op1.remove_old_hazards(purge=True)

        # op6 is at same path with delete range at lower index than
        # insert. (edge case)
        op6 = Op("ad", array_path, offset=1, val=2)
        op6.ot(cs1)
        assert op6.t_path == array_path
        assert op6.t_val == 2
        assert op6.t_offset == 1
        op1.remove_old_hazards(purge=True)

        # op7 path is in an array element being pushed forward (edge case)
        op7_path = array_path + [3, 9, "c"]
        op7 = Op("ad", op7_path, offset=8, val=3)
        op7.ot(cs1)
        assert op7.t_path == array_path + [5, 9, "c"]
        assert op7.t_offset == 8
        op1.remove_old_hazards(purge=True)

        # op8 path is in an array element being pushed forward (not edge case)
        op8_path = array_path + [5, 9, "c"]
        op8 = Op("ad", op8_path, offset=8, val=3)
        op8.ot(cs1)
        assert op8.t_path == array_path + [7, 9, "c"]
        op1.remove_old_hazards(purge=True)

        # op9 path is shorter then array's path, so no change
        op9_path = ["a", "b", 3]
        op9 = Op("ad", op9_path, offset=8, val=3)
        op9.ot(cs1)
        assert op9.t_path == op9_path
        assert op9.t_offset == 8
        op1.remove_old_hazards(purge=True)
    def test_multiple_css_with_same_multiple_dependencies(self):
        """
        A -- B -- D--F -- G
              \    \/   /
               \   /\  /
                - C--E-
        Both F and E depend on D and C
        """
        doc = Document(snapshot='')
        doc.HAS_EVENT_LOOP = False
        root = doc.get_root_changeset()
        A = Changeset(doc.get_id(), "user0", [root])
        A.set_id('a')
        doc.receive_changeset(A)
        assert doc.get_ordered_changesets() == [root, A]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        B = Changeset(doc.get_id(), "user1", [A])
        B.set_id('b')
        doc.receive_changeset(B)
        assert doc.get_ordered_changesets() == [root, A, B]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        C = Changeset(doc.get_id(), "user3", [B])
        C.set_id('c')
        doc.receive_changeset(C)
        assert doc.get_ordered_changesets() == [root, A, B, C]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        D = Changeset(doc.get_id(), "user4", [B])
        D.set_id('d')
        doc.receive_changeset(D)
        assert doc.get_ordered_changesets() == [root, A, B, C, D]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        E = Changeset(doc.get_id(), "user5", [C, D])
        E.set_id('e')
        doc.receive_changeset(E)
        assert doc.get_ordered_changesets() == [root, A, B, C, D, E]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        F = Changeset(doc.get_id(), "user6", [C, D])
        F.set_id('f')
        doc.receive_changeset(F)
        assert set(doc.get_dependencies()) == set([E,F])
        assert doc.get_ordered_changesets() == [root, A,B,C,D,E,F]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        G = Changeset(doc.get_id(), "user5", [E,F])
        doc.receive_changeset(G)
        assert doc.get_dependencies() == [G]
        assert doc.get_ordered_changesets() == [root, A, B, C, D, E, F, G]
        assert doc.get_ordered_changesets() == doc.tree_to_list()
    def test_one_delete_in_first_branch(self):
        """
        There is one delete in the first branch, multiple in branch B, then
        each has more string inserts.
        """
        doc = self.doc1
        root = self.root1

        # construct branch A, which begins with a string delete, then
        # adds text
        A0 = Changeset(doc.get_id(), 'u1', [root])
        A0.add_op(Op('ad', [], offset=3, val=2))
        A0.set_id('A')
        doc.receive_changeset(A0)

        A1 = Changeset(doc.get_id(), 'u1', [A0])
        v1 = ['0123', '4567']
        A1.add_op(Op('ai', [], offset=3, val=v1))
        doc.receive_changeset(A1)
        result = ['ABCD',
                  'EFGH',
                  'IJKL',
                  '0123',
                  '4567',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result

        # Branch B has common parent with A. B has three deletes, some of which
        # overlap the delete in branch A
        B0 = Changeset(doc.get_id(), 'u2', [root])
        opB0 = Op('ad', [], offset=2, val=2)
        B0.add_op(opB0)
        B0.set_id('B')
        doc.receive_changeset(B0)
        a_index = doc.get_ordered_changesets().index(A0)
        b_index = doc.get_ordered_changesets().index(B0)
        assert a_index < b_index
        assert opB0.t_offset == 2
        assert opB0.t_val == 1
        result = ['ABCD',
                  'EFGH',
                  '0123',
                  '4567',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result

        # Partially overlaping delete
        B1 = Changeset(doc.get_id(), 'u2', [B0])
        opB1 = Op('ad', [], offset=2, val=1)
        B1.add_op(opB1)
        B1.set_id('B1')
        doc.receive_changeset(B1)
        result = ['ABCD',
                  'EFGH',
                  '0123',
                  '4567',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result
        assert opB1.t_val == 0
        assert opB1.is_noop()

        # Delete Range unaffected by branch A
        B2 = Changeset(doc.get_id(), 'u2', [B1])
        opB2 = Op('ad', [], offset=1, val=1)
        B2.add_op(opB2)
        B2.set_id('B2')
        doc.receive_changeset(B2)
        result = ['ABCD',
                  '0123',
                  '4567',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result
        assert opB2.t_offset == 1
        assert opB2.t_val == 1

        # Insert before the Delete Range
        B3 = Changeset(doc.get_id(), 'u2', [B2])
        vB3 = ['BBBBB', 'CCCCC']
        opB3 = Op('ai', [], offset=1, val=vB3)
        B3.add_op(opB3)
        B3.set_id('B3')
        doc.receive_changeset(B3)
        result = ['ABCD',
                  '0123',
                  '4567',
                  'BBBBB',
                  'CCCCC',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result

        # Insert After the Delete Range
        B4 = Changeset(doc.get_id(), 'u2', [B3])
        vB4 = ['DDDDD', 'EEEEE']
        opB4 = Op('ai', [], offset=3, val=vB4)
        B4.add_op(opB4)
        B4.set_id('B4')
        doc.receive_changeset(B4)
        result = ['ABCD',
                  '0123',
                  '4567',
                  'BBBBB',
                  'CCCCC',
                  'DDDDD',
                  'EEEEE',
                  'UVWX',
                  'YZ']
        assert doc.get_snapshot() == result
    def test_complex_tree(self):
        """
        Some complex tree.

               C -- G -- H -------- K
              /         /            \
        A -- B -- D -- F -- J -- L -- M--
                   \       /
                    E --- I

        Should be:
        A B C G D E I F H K J L M
        """

        doc = Document(snapshot='')
        doc.HAS_EVENT_LOOP = False
        root = doc.get_root_changeset()
        A = Changeset(doc.get_id(), "user0", [root])
        A.set_id('A')
        doc.receive_changeset(A)
        assert doc.get_ordered_changesets() == [root, A]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        B = Changeset(doc.get_id(),"user1",[A])
        B.set_id('b')
        doc.receive_changeset(B)
        assert doc.get_ordered_changesets() == [root,A,B]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        C = Changeset(doc.get_id(),"user3",[B])
        C.set_id('c')
        doc.receive_changeset(C)
        assert doc.get_ordered_changesets() == [root,A,B,C]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        D = Changeset(doc.get_id(),"user4",[B])
        D.set_id('d')
        doc.receive_changeset(D)
        assert doc.get_ordered_changesets() == [root,A,B,C,D]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        E = Changeset(doc.get_id(),"user5",[D])
        E.set_id('e')
        doc.receive_changeset(E)
        assert doc.get_ordered_changesets() == [root,A,B,C,D,E]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        F = Changeset(doc.get_id(),"user6",[D])
        F.set_id('f')
        doc.receive_changeset(F)
        assert doc.get_ordered_changesets() == [root,A,B,C,D,E,F]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        G = Changeset(doc.get_id(),"user5",[C])
        G.set_id('g')
        doc.receive_changeset(G)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,F]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        H = Changeset(doc.get_id(),"user5",[G,F])
        H.set_id('h')
        doc.receive_changeset(H)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,F,H]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        I = Changeset(doc.get_id(),"user6",[E])
        I.set_id('i')
        doc.receive_changeset(I)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,I,F,H]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        J = Changeset(doc.get_id(),"user5",[I,F])
        J.set_id('j')
        doc.receive_changeset(J)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,I,F,H,J]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        K = Changeset(doc.get_id(),"user5",[H])
        K.set_id('k')
        doc.receive_changeset(K)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,I,F,H,K,J]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        L = Changeset(doc.get_id(),"user5",[J])
        L.set_id('l')
        doc.receive_changeset(L)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,I,F,H,K,J,L]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        M = Changeset(doc.get_id(),"user5",[K,L])
        M.set_id('m')
        doc.receive_changeset(M)
        assert doc.get_ordered_changesets() == [root,A,B,C,G,D,E,I,F,H,K,J,L,M]
        assert doc.get_ordered_changesets() == doc.tree_to_list()
    def test_ad_ai(self):
        """
        A past opperations is an array delete which gets applied to this op.
        """
        array_path = ["a", "b", 3, 4]
        op1 = Op("ad", array_path, offset=3, val=3)
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected

        op2 = Op("ai", ["a", "b", 3, 5], offset=2, val=["XYZ"])
        op2.ot(cs1)
        assert op2.t_path == ["a", "b", 3, 5]
        assert op2.t_offset == 2
        op1.remove_old_hazards(purge=True)

        # op3 happens at path, but lower offset, no change (edge case)
        op3 = Op("ai", array_path, offset=2, val=["XYZ"])
        op3.ot(cs1)
        assert op3.t_path == array_path
        assert op3.t_offset == 2
        assert not op3.is_noop()
        op1.remove_old_hazards(purge=True)

        # op4 happens at path and in deletion range (edge case)
        op4 = Op("ai", array_path, offset=4, val=["XYZ"])
        op4.ot(cs1)
        assert op4.t_path == array_path
        assert op4.t_offset == 3
        assert op4.is_noop()
        op1.remove_old_hazards(purge=True)

        # op5 happens at path and after deletion range (edge case)
        op5 = Op("ai", array_path, offset=6, val=["XYZ"])
        op5.ot(cs1)
        assert op5.t_path == array_path
        assert op5.t_offset == 3
        assert not op5.is_noop()
        op1.remove_old_hazards(purge=True)

        # op5 happens within an array element being deleted (edge case)
        op5_path = array_path + [3]
        op5 = Op("ai", op5_path, offset=8, val=["XYZ"])
        op5.ot(cs1)
        assert op5.is_noop()
        op1.remove_old_hazards(purge=True)

        # op6 is within an array element being deleted (other edge case)
        op6_path = array_path + [5]
        op6 = Op("ai", op6_path, offset=8, val=["XYZ"])
        op6.ot(cs1)
        assert op6.is_noop()
        op1.remove_old_hazards(purge=True)

        # op7 path is far in an array element being deleted
        op7_path = array_path + [4, 9, "c"]
        op7 = Op("ai", op7_path, offset=8, val=["XYZ"])
        op7.ot(cs1)
        assert op7.is_noop()
        op1.remove_old_hazards(purge=True)

        # op8 path is in an array element being pulled back (edge case)
        op8_path = array_path + [6, 9, "c"]
        op8 = Op("ai", op8_path, offset=8, val=["XYZ"])
        op8.ot(cs1)
        assert op8.t_path == array_path + [3, 9, "c"]
        op1.remove_old_hazards(purge=True)

        # op9 path is in an array element NOT being pulled back (edge case)
        op9_path = array_path + [2, 9, "c"]
        op9 = Op("ai", op9_path, offset=8, val=["XYZ"])
        op9.ot(cs1)
        assert op9.t_path == array_path + [2, 9, "c"]
        op1.remove_old_hazards(purge=True)

        # op10 path is shorter than past path, so no change
        op10_path = ["a", "b", 3]
        op10 = Op("ai", op10_path, offset=8, val=["XYZ"])
        op10.ot(cs1)
        assert op10.t_path == ["a", "b", 3]
        assert op10.t_offset == 8
        op1.remove_old_hazards(purge=True)
    def test_ad_ad_same_arrays(self):
        """
        A past opperation is an array delete which gets applied to these array
        deletes. In these cases, they ops are working on the same arrays, so
        their paths should be unaffected. Only their offsets and vals should
        shift because of potential overlapping delete ranges.
        """
        path = ["a", "b", 3, 4]
        op1 = Op("ad", path, offset=5, val=10)
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        #                |-- prev op --|
        # |-- self --|
        # op2 happens at lower offset, so should not be affected
        op2 = Op("ad", path, offset=1, val=3)
        op2.ot(cs1)
        assert op2.t_path == path
        assert op2.t_offset == 1
        assert op2.t_val == 3
        op1.remove_old_hazards(purge=True)

        #            |-- prev op --|
        # |-- self --|
        # op3 happens at lower offset, so should not be affected (edge case)
        op3 = Op("ad", path, offset=2, val=3)
        op3.ot(cs1)
        assert op3.t_path == path
        assert op3.t_offset == 2
        assert op3.t_val == 3
        op1.remove_old_hazards(purge=True)

        #  |-- prev op --|
        #                   |-- self --|
        # op4 is above delete range
        op4 = Op("ad", path, offset=20, val=5)
        op4.ot(cs1)
        assert op4.t_offset == 10
        assert op4.t_val == 5
        op1.remove_old_hazards(purge=True)

        #  |-- prev op --|
        #                |-- self --|
        # op5 is above delete range (edge case)
        op5 = Op("ad", path, offset=15, val=3)
        op5.ot(cs1)
        assert op5.t_offset == 5
        assert op5.t_val == 3
        assert not op5.is_noop()
        op1.remove_old_hazards(purge=True)

        #           |-- prev op --|
        #      |-- self --|
        # ops partially overlap
        op6 = Op("ad", path, offset=3, val=7)
        op6.ot(cs1)
        assert op6.t_offset == 3
        assert op6.t_val == 2
        op1.remove_old_hazards(purge=True)

        #    |-- prev op --|
        #            |-- self --|
        # ops partially overlap
        op9 = Op("ad", path, offset=11, val=10)
        op9.ot(cs1)
        assert op9.t_offset == 5
        assert op9.t_val == 6
        op1.remove_old_hazards(purge=True)

        #     |-- prev op --|
        #     |--   self  --|
        # ops perfectly overlap
        op10 = Op("ad", path, offset=5, val=10)
        op10.ot(cs1)
        assert op10.t_offset == 5
        assert op10.t_val == 0
        op1.remove_old_hazards(purge=True)

        #      |--  prev op  --|
        #        |-- self --|
        # prev encompases self
        op11 = Op("ad", path, offset=7, val=5)
        op11.ot(cs1)
        assert op11.t_offset == 5
        assert op11.t_val == 0
        op1.remove_old_hazards(purge=True)

        #    |-- prev op --|
        #    |-- self --|
        # prev encompases self (edge case)
        op12 = Op("ad", path, offset=5, val=6)
        op12.ot(cs1)
        assert op12.t_offset == 5
        assert op12.t_val == 0
        op1.remove_old_hazards(purge=True)

        #    |-- prev op --|
        #       |-- self --|
        # prev encompases self (other edge case)
        op12 = Op("ad", path, offset=10, val=5)
        op12.ot(cs1)
        assert op12.t_offset == 5
        assert op12.t_val == 0
        op1.remove_old_hazards(purge=True)

        #    |-- prev op --|
        #        |-- self --|
        # self deletes one more than prev (edge case)
        op13 = Op("ad", path, offset=10, val=6)
        op13.ot(cs1)
        assert op13.t_offset == 5
        assert op13.t_val == 1
        op1.remove_old_hazards(purge=True)

        #      |-- prev op --|
        #    |--    self     --|
        # self encompases prev
        op14 = Op("ad", path, offset=3, val=20)
        op14.ot(cs1)
        assert op14.t_offset == 3
        assert op14.t_val == 10
        op1.remove_old_hazards(purge=True)

        #      |-- prev op --|
        #      |--    self    --|
        # self encompases prev (edge case)
        oop15 = Op("ad", path, offset=5, val=12)
        oop15.ot(cs1)
        assert oop15.t_offset == 5
        assert oop15.t_val == 2
        op1.remove_old_hazards(purge=True)

        #       |-- prev op --|
        #     |--   self    --|
        # self encompases prev (other edge case)
        op16 = Op("ad", path, offset=4, val=11)
        op16.ot(cs1)
        assert op16.t_offset == 4
        assert op16.t_val == 1
        op1.remove_old_hazards(purge=True)

        #       |-- prev op --|
        #     |--    self    --|
        # self encompases prev (off by one on bother sides)
        op17 = Op("ad", path, offset=4, val=12)
        op17.ot(cs1)
        assert op17.t_offset == 4
        assert op17.t_val == 2
        op1.remove_old_hazards(purge=True)
    def test_multiple_dependencies(self):
        """
             -- B ---- E
            /         /
        root -- A -- D
            \       /
             -- C --
        """
        doc = Document(snapshot='')
        doc.HAS_EVENT_LOOP = False
        root = doc.get_root_changeset()
        B = Changeset(doc.get_id(), "user0", [root])
        B.set_id('b')
        doc.receive_changeset(B)

        A = Changeset(doc.get_id(), "user1", [root])
        A.set_id('a')
        doc.receive_changeset(A)
        assert set(doc.get_dependencies()) == set([B,A])
        assert doc.get_ordered_changesets() == [root, A, B]

        C = Changeset(doc.get_id(), "user2", [root])
        C.set_id('c')
        doc.receive_changeset(C)
        assert set(doc.get_dependencies()) == set([B,A,C])
        assert doc.get_ordered_changesets() == [root, A, B, C]
        assert doc.get_ordered_changesets() == doc.tree_to_list()


        # test_multiple_dependencies_common_base
        D = Changeset(doc.get_id(), "user0", [C,A])
        D.set_id('d')
        doc.receive_changeset(D)
        assert set(doc.get_dependencies()) == set([B,D])
        assert doc.get_ordered_changesets() == [root, A, B, C, D]
        assert doc.get_ordered_changesets() == doc.tree_to_list()

        E = Changeset(doc.get_id(), 'user1', [B, D])
        E.set_id('e')
        doc.receive_changeset(E)
        assert doc.get_dependencies() == [E]
        assert doc.get_ordered_changesets() == [root, A, B, C, D, E]
        assert doc.get_ordered_changesets() == doc.tree_to_list()
    def test_ad_ad_different_arrays(self):
        """
        A past opperation is an array delete which gets applied to these array
        deletes. In each case, the are at different arrays, so there should be
        no changes to offset or val -- only path.
        """
        array_path = ["a", "b", 3, 4]
        op1 = Op("ad", array_path, offset=3, val=3)
        cs1 = Changeset("doc_id", "author", [])
        cs1.add_op(op1)

        # op2 happens at a different path, so should not be affected
        op2 = Op("ad", ["a", "b", 3, 5], offset=2, val=3)
        op2.ot(cs1)
        assert op2.t_path == ["a", "b", 3, 5]
        assert op2.t_offset == 2
        assert op2.t_val == 3
        op1.remove_old_hazards(purge=True)

        # op3 happens along path of previous delete but in an unaffected
        # element (edge case)
        op3_path = array_path + [2]
        op3 = Op("ad", op3_path, offset=6, val=5)
        op3.ot(cs1)
        assert op3.t_path == array_path + [2]
        assert op3.t_offset == 6
        assert op3.t_val == 5
        op1.remove_old_hazards(purge=True)

        # op4 happens along path of previous delete in an element that gets
        # shifted back (edge case)
        op4_path = array_path + [6]
        op4 = Op("ad", op4_path, offset=12, val=13)
        op4.ot(cs1)
        assert op4.t_path == array_path + [3]
        assert op4.t_val == 13
        assert op4.t_offset == 12
        op1.remove_old_hazards(purge=True)

        # op5 happens along path of previous delete and in an affected element
        # (edge case)
        op5_path = array_path + [3]
        op5 = Op("ad", op5_path, offset=1, val=3)
        op5.ot(cs1)
        assert op5.is_noop()
        op1.remove_old_hazards(purge=True)

        # op6 happens along path of previous delete and in an affected element
        # (other edge case)
        op6_path = array_path + [5]
        op6 = Op("ad", op6_path, offset=1, val=2)
        op6.ot(cs1)
        assert op6.is_noop()
        op1.remove_old_hazards(purge=True)

        # op9 path is shorter then array's path, so no change
        op9_path = ["a", "b", 3]
        op9 = Op("ad", op9_path, offset=8, val=3)
        op9.ot(cs1)
        assert op9.t_path == op9_path
        assert op9.t_offset == 8
        assert op9.t_val == 3
        op1.remove_old_hazards(purge=True)