def test_remove_version_1_nfa_2_versions_1_event(self):
        buffer = SharedVersionedMatchBuffer()

        # two runs for the same nfa
        run_id_a = generate_unique_string()
        run_id_b = generate_unique_string()

        # version for each run
        version_a = RunVersion()
        version_a.add_level(run_id_a)
        version_a_str = version_a.get_version_as_str()

        version_b = RunVersion()
        version_b.add_level(run_id_b)
        version_b_str = version_b.get_version_as_str()

        # put event for run a
        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_B,
                         event=event_a)

        # match event for event a, run a
        match_event = buffer._eve[NFA_NAME_A][LABEL_LAYER_B][event_a.event_id]
        self.assertIsNotNone(match_event)
        self.assertEqual(event_a, match_event.event)
        self.assertTrue(version_a_str in match_event.next_ids)

        # put event for run b
        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_b,
                         version=version_b_str,
                         state_label=LABEL_LAYER_B,
                         event=event_a)

        # run b in same match event
        self.assertTrue(version_b_str in match_event.next_ids)

        # remove version a, match event should remain for version b
        buffer.remove_version(nfa_name=NFA_NAME_A, version=version_a_str)

        match_event = buffer._eve[NFA_NAME_A][LABEL_LAYER_B][event_a.event_id]
        self.assertIsNotNone(match_event)
        self.assertFalse(version_a_str in match_event.next_ids)
        self.assertTrue(version_b_str in match_event.next_ids)

        # remove version b, match event should be removed from the buffer
        buffer.remove_version(nfa_name=NFA_NAME_A, version=version_b_str)

        with self.assertRaises(KeyError):
            match_event = buffer._eve[NFA_NAME_A][LABEL_LAYER_B][
                event_a.event_id]
        self.assertFalse(version_b_str in match_event.next_ids)
    def test_clear_all_levels(self):
        version = RunVersion()

        version.add_level('abc')
        version.add_level('def')
        version.add_level('ghi')

        self.assertEqual(3, version.size())

        version.remove_all_levels()
        self.assertEqual(0, version.size())
Exemple #3
0
    def test_to_dict_two_match_events(self):
        event_a = PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())
        event_b = PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())

        match_a = MatchEvent(nfa_name=NFA_NAME_A,
                             label=LABEL_LAYER_A,
                             event=event_a)
        match_b = MatchEvent(nfa_name=NFA_NAME_A,
                             label=LABEL_LAYER_B,
                             event=event_b)

        version = RunVersion()
        version.add_level(
            BoboRun._generate_id(nfa_name=NFA_NAME_A,
                                 start_event_id=event_a.event_id))
        version_str = version.get_version_as_str()

        # match a --next--> match b
        match_a.add_pointer_next(version=version_str,
                                 label=match_b.label,
                                 event_id=match_b.event.event_id)

        # match a dict
        self.assertDictEqual(
            match_a.to_dict(), {
                MatchEvent.NFA_NAME: NFA_NAME_A,
                MatchEvent.LABEL: LABEL_LAYER_A,
                MatchEvent.EVENT: event_a.to_dict(),
                MatchEvent.NEXT_IDS: {
                    version_str: (match_b.label, match_b.event.event_id)
                },
                MatchEvent.PREVIOUS_IDS: {}
            })

        # match a <--previous-- match b
        match_b.add_pointer_previous(version=version_str,
                                     label=match_a.label,
                                     event_id=match_a.event.event_id)

        # match b dict
        self.assertDictEqual(
            match_b.to_dict(), {
                MatchEvent.NFA_NAME: NFA_NAME_A,
                MatchEvent.LABEL: LABEL_LAYER_B,
                MatchEvent.EVENT: event_b.to_dict(),
                MatchEvent.NEXT_IDS: {},
                MatchEvent.PREVIOUS_IDS: {
                    version_str: (match_a.label, match_a.event.event_id)
                }
            })
    def test_1_level_10_increments_1_event_per_increment(self):
        buffer = SharedVersionedMatchBuffer()
        run_id = generate_unique_string()

        events = [
            PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())
            for _ in range(10)
        ]

        version = RunVersion()
        version.add_level(run_id)
        version_current = version.get_version_as_str()

        for event in events:
            version.increment_level(generate_unique_string())
            version_next = version.get_version_as_str()

            buffer.put_event(nfa_name=NFA_NAME_A,
                             run_id=run_id,
                             version=version_current,
                             state_label=LABEL_LAYER_A,
                             event=event,
                             new_version=version_next)

            version_current = version_next

        history_events = buffer.get_all_events(
            nfa_name=NFA_NAME_A, run_id=run_id,
            version=version).events[LABEL_LAYER_A]

        self.assertEqual(10, len(history_events))

        for event in events:
            self.assertTrue(event in history_events)
    def run(d: dict, buffer: SharedVersionedMatchBuffer,
            nfa: BoboNFA) -> 'BoboRun':
        """
        :param d: A dict representation of a BoboRun instance.
        :type d: dict

        :param buffer: A buffer to use with the new BoboRun instance.
        :type buffer: SharedVersionedMatchBuffer

        :param nfa: An automaton to use with the new BoboRun instance.
        :type nfa: BoboNFA

        :return: A new BoboRun instance.
        """

        event = BoboRuleBuilder.event(d[BoboRun.EVENT])
        start_time = d[BoboRun.START_TIME]
        start_state = nfa.states[d[BoboRun.START_STATE_NAME]]
        current_state = nfa.states[d[BoboRun.CURRENT_STATE_NAME]]
        run_id = d[BoboRun.RUN_ID]
        version = RunVersion.list_to_version(d[BoboRun.VERSION])
        last_proceed_had_clone = d[BoboRun.LAST_PROCESS_CLONED]
        halted = d[BoboRun.HALTED]

        return BoboRun(buffer=buffer,
                       nfa=nfa,
                       event=event,
                       start_time=start_time,
                       start_state=start_state,
                       current_state=current_state,
                       run_id=run_id,
                       version=version,
                       put_event=False,
                       last_process_cloned=last_proceed_had_clone,
                       halted=halted)
Exemple #6
0
    def test_remove_all_pointers_two_match_events(self):
        event_a = PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())
        event_b = PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())

        match_a = MatchEvent(nfa_name=NFA_NAME_A,
                             label=LABEL_LAYER_A,
                             event=event_a)
        match_b = MatchEvent(nfa_name=NFA_NAME_A,
                             label=LABEL_LAYER_B,
                             event=event_b)

        version = RunVersion()
        version.add_level(
            BoboRun._generate_id(nfa_name=NFA_NAME_A,
                                 start_event_id=event_a.event_id))
        version_str = version.get_version_as_str()

        # match events should start with no pointers
        self.assertFalse(match_a.has_pointers())
        self.assertFalse(match_b.has_pointers())

        # match a --next--> match b
        match_a.add_pointer_next(version=version_str,
                                 event_id=match_b.event.event_id)

        # match a <--previous-- match b
        match_b.add_pointer_previous(version=version_str,
                                     event_id=match_a.event.event_id)

        # match events both have pointers
        self.assertTrue(match_a.has_pointers())
        self.assertTrue(match_b.has_pointers())

        # removing pointers from one match event
        match_a.remove_all_pointers(version=version_str)

        self.assertFalse(match_a.has_pointers())
        self.assertTrue(match_b.has_pointers())

        # all pointers removed
        match_b.remove_all_pointers(version=version_str)

        self.assertFalse(match_a.has_pointers())
        self.assertFalse(match_b.has_pointers())
Exemple #7
0
    def test_match_event_points_to_itself(self):
        event_a = PrimitiveEvent(timestamp=EpochNSClock.generate_timestamp())

        match_a = MatchEvent(nfa_name=NFA_NAME_A,
                             label=LABEL_LAYER_A,
                             event=event_a)

        version = RunVersion()
        version.add_level(
            BoboRun._generate_id(nfa_name=NFA_NAME_A,
                                 start_event_id=event_a.event_id))
        version_str = version.get_version_as_str()

        with self.assertRaises(RuntimeError):
            match_a.add_pointer_next(version=version_str,
                                     label=LABEL_LAYER_A,
                                     event_id=event_a.event_id)

        with self.assertRaises(RuntimeError):
            match_a.add_pointer_previous(version=version_str,
                                         label=LABEL_LAYER_A,
                                         event_id=event_a.event_id)
    def test_put_and_get_event(self):
        buffer = SharedVersionedMatchBuffer()
        run_id = generate_unique_string()
        version = RunVersion()
        version.add_level(run_id)

        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id,
                         version=version.get_version_as_str(),
                         state_label=LABEL_LAYER_A,
                         event=event_a)

        self.assertEqual(
            event_a,
            buffer.get_event(nfa_name=NFA_NAME_A,
                             state_label=LABEL_LAYER_A,
                             event_id=event_a.event_id,
                             default=None))

        # incorrect nfa name
        self.assertIsNone(
            buffer.get_event(nfa_name=NFA_NAME_B,
                             state_label=LABEL_LAYER_A,
                             event_id=event_a.event_id,
                             default=None))

        # incorrect state label
        self.assertIsNone(
            buffer.get_event(nfa_name=NFA_NAME_A,
                             state_label=LABEL_LAYER_B,
                             event_id=event_a.event_id,
                             default=None))

        # incorrect event id
        self.assertIsNone(
            buffer.get_event(nfa_name=NFA_NAME_A,
                             state_label=LABEL_LAYER_A,
                             event_id=event_b.event_id,
                             default=None))
    def test_add_level(self):
        version = RunVersion()

        version.add_level('abc')
        self.assertListEqual([['abc']], version._levels)

        version.add_level('def')
        self.assertListEqual([['abc'], ['def']], version._levels)
    def test_list_to_version(self):
        strlist = ['abc', 'def']

        self.assertListEqual(strlist,
                             RunVersion.list_to_version(strlist)._levels)
    def test_increment_level(self):
        version = RunVersion()

        with self.assertRaises(RuntimeError):
            version.increment_level('abc')

        version.add_level('abc')
        version.increment_level('def')

        self.assertListEqual([['abc', 'def']], version._levels)

        version.add_level('ghi')
        version.increment_level('jkl')

        self.assertListEqual([['abc', 'def'], ['ghi', 'jkl']], version._levels)
    def test_constructor_existing(self):
        existing = RunVersion()
        existing.add_level('abc')

        version = RunVersion(parent_version=existing)
        self.assertListEqual([['abc']], version._levels)
 def test_constructor_no_existing(self):
     version = RunVersion()
     self.assertListEqual([], version._levels)
    def test_get_previous_version_as_str(self):
        version = RunVersion()

        # No levels or increments exist yet
        self.assertEqual(
            "",
            version.get_previous_version_as_str(decrease_level=0,
                                                decrease_incr=0))

        # Add levels and increments
        version.add_level('abc')
        version.add_level('def')
        version.increment_level('ghi')

        # Check current version is correct
        self.assertEqual('abc.ghi', version.get_version_as_str())

        # Go back one increment
        self.assertEqual(
            'abc.def',
            version.get_previous_version_as_str(decrease_level=0,
                                                decrease_incr=1))

        # No more previous increments in current level
        self.assertIsNone(
            version.get_previous_version_as_str(decrease_level=0,
                                                decrease_incr=2))

        # Go back one level
        self.assertEqual(
            'abc',
            version.get_previous_version_as_str(decrease_level=1,
                                                decrease_incr=0))

        # No more previous increments in previous level
        self.assertIsNone(
            version.get_previous_version_as_str(decrease_level=1,
                                                decrease_incr=1))

        # No more previous levels
        self.assertIsNone(
            version.get_previous_version_as_str(decrease_level=2,
                                                decrease_incr=0))
    def test_to_dict_1_nfa_1_version_3_events_3_labels(self):
        buffer = SharedVersionedMatchBuffer()
        run_id_a = generate_unique_string()

        version_a = RunVersion()
        version_a.add_level(run_id_a)
        version_a_str = version_a.get_version_as_str()

        # add three events to buffer for version a, run a
        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_A,
                         event=event_a)

        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_B,
                         event=event_b)

        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_C,
                         event=event_c)

        # check that it is a dict type
        d = buffer.to_dict()
        self.assertIsInstance(d, dict)

        events = d[SharedVersionedMatchBuffer.EVENTS]
        last = d[SharedVersionedMatchBuffer.LAST]
        labels = []

        # check events
        for event in events:
            self.assertEqual(NFA_NAME_A,
                             event[SharedVersionedMatchBuffer.NFA_NAME])

            # check that the right event is paired with its label
            match_ev = event[SharedVersionedMatchBuffer.MATCH_EVENT]

            # check label
            label = match_ev[MatchEvent.LABEL]
            self.assertTrue((label == LABEL_LAYER_A or label == LABEL_LAYER_B
                             or label == LABEL_LAYER_C)
                            and label not in labels)
            labels.append(label)

            if label == LABEL_LAYER_A:
                self.assertDictEqual(event_a.to_dict(),
                                     match_ev[MatchEvent.EVENT])

            elif label == LABEL_LAYER_B:
                self.assertDictEqual(event_b.to_dict(),
                                     match_ev[MatchEvent.EVENT])

            elif label == LABEL_LAYER_C:
                self.assertDictEqual(event_c.to_dict(),
                                     match_ev[MatchEvent.EVENT])

        match_ev_ids = []

        # check last event
        for event in last:
            self.assertEqual(NFA_NAME_A,
                             event[SharedVersionedMatchBuffer.NFA_NAME])
            self.assertEqual(run_id_a,
                             event[SharedVersionedMatchBuffer.RUN_ID])
            self.assertEqual(version_a_str,
                             event[SharedVersionedMatchBuffer.VERSION])

            match_ev_id = event[SharedVersionedMatchBuffer.EVENT_ID]
            self.assertTrue(match_ev_id not in match_ev_ids)
            match_ev_ids.append(match_ev_id)
    def test_get_previous_version_as_list(self):
        version = RunVersion()

        # No levels or increments exist yet
        with self.assertRaises(RuntimeError):
            version.get_previous_version_as_list(decrease_level=0,
                                                 decrease_incr=0)

        # Add levels and increments
        version.add_level('abc')
        version.add_level('def')
        version.increment_level('ghi')

        # Check current version is correct
        self.assertListEqual(['abc', 'ghi'], version.get_version_as_list())

        # Go back one increment
        self.assertListEqual(['abc', 'def'],
                             version.get_previous_version_as_list(
                                 decrease_level=0, decrease_incr=1))

        # No more previous increments in current level
        with self.assertRaises(RuntimeError):
            version.get_previous_version_as_list(decrease_level=0,
                                                 decrease_incr=2)

        # Go back one level
        self.assertListEqual(['abc'],
                             version.get_previous_version_as_list(
                                 decrease_level=1, decrease_incr=0))

        # No more previous increments in previous level
        with self.assertRaises(RuntimeError):
            version.get_previous_version_as_list(decrease_level=1,
                                                 decrease_incr=1)

        # No more previous levels
        with self.assertRaises(RuntimeError):
            version.get_previous_version_as_list(decrease_level=2,
                                                 decrease_incr=0)
Exemple #17
0
    def __init__(self,
                 buffer: SharedVersionedMatchBuffer,
                 nfa: BoboNFA,
                 event: BoboEvent,
                 start_time: int = None,
                 start_state: BoboState = None,
                 current_state: BoboState = None,
                 parent_run: 'BoboRun' = None,
                 run_id: str = None,
                 version: RunVersion = None,
                 put_event: bool = True,
                 last_process_cloned: bool = False,
                 halted: bool = False) -> None:

        super().__init__()

        self.buffer = buffer
        self.nfa = nfa
        self.event = event
        self.start_time = start_time if start_time is not None \
            else event.timestamp
        self.start_state = start_state if start_state is not None \
            else self.nfa.start_state
        self.current_state = current_state if current_state is not None \
            else self.start_state
        self.id = BoboRun._generate_id(
            nfa_name=self.nfa.name,
            start_event_id=self.event.event_id) \
            if run_id is None else run_id
        self.version = version
        self._last_process_cloned = last_process_cloned
        self._halted = halted
        self._final = False
        self._subs = []
        self._lock = RLock()

        if parent_run is None:
            # create new version and add new event under this version
            if self.version is None:
                self.version = RunVersion()
                self.version.add_level(self.id)

            if put_event:
                self.buffer.put_event(
                    nfa_name=self.nfa.name,
                    run_id=self.id,
                    version=self.version.get_version_as_str(),
                    state_label=self.current_state.label,
                    event=self.event)

        else:
            # create version from existing, and
            # put and link last event to new version
            if self.version is None:
                self.version = RunVersion(
                    parent_version=parent_run.version)
                self.version.add_level(self.id)

            if put_event:
                self.buffer.put_event(
                    nfa_name=self.nfa.name,
                    run_id=parent_run.id,
                    version=parent_run.version.get_version_as_str(),
                    state_label=self.current_state.label,
                    event=self.event,
                    new_run_id=self.id,
                    new_version=self.version.get_version_as_str())

        # immediately final if start state is final state
        if self.nfa.start_is_final:
            self.set_final(history=None, notify=False)
    def test_get_version_as_list(self):
        version = RunVersion()

        self.assertListEqual([], version.get_version_as_list())

        version.add_level('abc')

        self.assertListEqual(['abc'], version.get_version_as_list())

        version.add_level('def')

        self.assertListEqual(['abc', 'def'], version.get_version_as_list())

        version.increment_level('ghi')

        self.assertListEqual(['abc', 'ghi'], version.get_version_as_list())
Exemple #19
0
    def get_all_events(self, nfa_name: str, run_id: str,
                       version: RunVersion) -> BoboHistory:
        """
        Gets all events associated with a run and compiles them into a
        BoboHistory instance.

        :param nfa_name: The BoboNFA instance name.
        :type nfa_name: str

        :param run_id: The run ID.
        :type run_id: str

        :param version: The run version.
        :type version: RunVersion

        :return: A BoboHistory instance with all of the events in it.
        """

        all_events = {}
        current_level = 0
        current_incr = 0
        current_version = version.get_version_as_str()

        # start with the latest match event
        current_event = self.get_last_event(nfa_name=nfa_name,
                                            run_id=run_id,
                                            version=current_version)

        while True:
            if current_event is not None:
                # add event to dict, keyed under the label name
                if current_event.label not in all_events:
                    all_events[current_event.label] = []
                all_events[current_event.label].insert(0, current_event.event)

                # get next match event using current version
                next_event = self._get_next_event(event=current_event,
                                                  nfa_name=nfa_name,
                                                  version_str=current_version)

                # no event found under current version
                if next_event is None:
                    # get previous version by decreasing increment
                    current_incr += 1
                    current_version = \
                        version.get_previous_version_as_str(
                            decrease_level=current_level,
                            decrease_incr=current_incr)

                    # get previous version by decreasing level
                    if current_version is None:
                        current_level += 1
                        current_incr = 0
                        current_version = \
                            version.get_previous_version_as_str(
                                decrease_level=current_level,
                                decrease_incr=current_incr)

                        # no previous version, stop search
                        if current_version is None:
                            break

                    # attempt to find next event with new version
                    next_event = self._get_next_event(
                        event=current_event,
                        nfa_name=nfa_name,
                        version_str=current_version)

                    if next_event is None:
                        break

                current_event = next_event
            else:
                break

        return BoboHistory(events=all_events)
    def test_2_nfas_2_versions_1_increment(self):
        buffer = SharedVersionedMatchBuffer()

        run_id_a = generate_unique_string()
        run_id_a_incr = generate_unique_string()
        run_id_b = generate_unique_string()

        # create two versions for run a and run b
        version_a = RunVersion()
        version_a.add_level(run_id_a)
        version_a_str = version_a.get_version_as_str()

        version_b = RunVersion()
        version_b.add_level(run_id_b)

        # add event a into version a, run a
        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_A,
                         event=event_a)

        # increment version a
        version_a.increment_level(generate_unique_string())
        version_a_incr_str = version_a.get_version_as_str()

        # add event b to version a incr, run a incr
        buffer.put_event(nfa_name=NFA_NAME_A,
                         run_id=run_id_a,
                         version=version_a_str,
                         state_label=LABEL_LAYER_B,
                         event=event_b,
                         new_run_id=run_id_a_incr,
                         new_version=version_a_incr_str)

        # add event c to version b, run b
        buffer.put_event(nfa_name=NFA_NAME_B,
                         run_id=run_id_b,
                         version=version_b.get_version_as_str(),
                         state_label=LABEL_LAYER_C,
                         event=event_c)

        # should only return events associated with version a and incr
        history_1 = buffer.get_all_events(nfa_name=NFA_NAME_A,
                                          run_id=run_id_a_incr,
                                          version=version_a)

        self.assertDictEqual(history_1.events, {
            LABEL_LAYER_A: [event_a],
            LABEL_LAYER_B: [event_b]
        })

        # should only return events associated with version 2 and run 2
        history_2 = buffer.get_all_events(nfa_name=NFA_NAME_B,
                                          run_id=run_id_b,
                                          version=version_b)

        self.assertDictEqual(history_2.events, {LABEL_LAYER_C: [event_c]})
    def test_size_level(self):
        version = RunVersion()

        version.add_level('abc')
        self.assertEqual(1, version.size_level(0))
        self.assertEqual(1, version.size_level())

        version.add_level('def')
        self.assertEqual(1, version.size_level(1))
        self.assertEqual(1, version.size_level())

        version.increment_level('ghi')
        self.assertEqual(2, version.size_level(1))
        self.assertEqual(2, version.size_level())
    def test_get_version_as_str(self):
        version = RunVersion()

        self.assertEqual('', version.get_version_as_str())

        version.add_level('abc')

        self.assertEqual('abc', version.get_version_as_str())

        version.add_level('def')

        self.assertEqual('abc.def', version.get_version_as_str())

        version.increment_level('ghi')

        self.assertEqual('abc.ghi', version.get_version_as_str())
    def test_str_to_version(self):
        strlist = ['abc', 'def']
        strver = 'abc.def'

        self.assertListEqual(strlist,
                             RunVersion.str_to_version(strver)._levels)
    def test_list_to_version_str(self):
        strlist = ['abc', 'def']

        self.assertEqual('abc.def', RunVersion.list_to_version_str(strlist))
Exemple #25
0
class BoboRun:
    """A :code:`bobocep` antomaton run.

    :param buffer: The buffer in which run data will be stored..
    :type buffer: SharedVersionedMatchBuffer

    :param nfa: The automaton with which the run is associated.
    :type nfa: BoboNFA

    :param event: The run event.
    :type event: BoboEvent

    :param start_time: The time when the run was created. It is used for
                       generating the run ID. Defaults to the event
                       instance's timestamp.
    :type start_time: int, optional

    :param start_state: The start state of the run, defaults to the NFA's start
                        state.
    :type start_state: BoboState, optional

    :param current_state: The current state of the run, defaults to the NFA's
                          start state.
    :type current_state: BoboState, optional

    :param parent_run: The run which is this run's parent. It is used when a
                       run has been created as a consequence of non-determinism
                       (i.e. run cloning). Defaults to None.
    :type parent_run: BoboRun, optional

    :param run_id: The ID of the run. Defaults to a run ID consisting of the
                   NFA name and start time.
    :type run_id: str, optional

    :param version: The run version. If a parent run is provided, the version
                    is generated relative to the parent version.
                    Defaults to the run ID.
    :type version: RunVersion, optional

    :param put_event: Puts the run event into the buffer on instantiation,
                      defaults to True.
    :type put_event: bool, optional

    :param last_process_cloned: The last time the run processed an event,
                                a clone occurred, defaults to False.
    :type last_process_cloned: bool, optional

    :param halted: The run is halted, defaults to False.
    :type halted: bool, optional
    """

    NFA_NAME = "nfa_name"
    EVENT = "event"
    START_TIME = "start_time"
    START_STATE_NAME = "start_state_name"
    CURRENT_STATE_NAME = "current_state_name"
    RUN_ID = "run_id"
    VERSION = "version"
    HALTED = "halted"
    LAST_PROCESS_CLONED = "last_process_cloned"

    def __init__(self,
                 buffer: SharedVersionedMatchBuffer,
                 nfa: BoboNFA,
                 event: BoboEvent,
                 start_time: int = None,
                 start_state: BoboState = None,
                 current_state: BoboState = None,
                 parent_run: 'BoboRun' = None,
                 run_id: str = None,
                 version: RunVersion = None,
                 put_event: bool = True,
                 last_process_cloned: bool = False,
                 halted: bool = False) -> None:

        super().__init__()

        self.buffer = buffer
        self.nfa = nfa
        self.event = event
        self.start_time = start_time if start_time is not None \
            else event.timestamp
        self.start_state = start_state if start_state is not None \
            else self.nfa.start_state
        self.current_state = current_state if current_state is not None \
            else self.start_state
        self.id = BoboRun._generate_id(
            nfa_name=self.nfa.name,
            start_event_id=self.event.event_id) \
            if run_id is None else run_id
        self.version = version
        self._last_process_cloned = last_process_cloned
        self._halted = halted
        self._final = False
        self._subs = []
        self._lock = RLock()

        if parent_run is None:
            # create new version and add new event under this version
            if self.version is None:
                self.version = RunVersion()
                self.version.add_level(self.id)

            if put_event:
                self.buffer.put_event(
                    nfa_name=self.nfa.name,
                    run_id=self.id,
                    version=self.version.get_version_as_str(),
                    state_label=self.current_state.label,
                    event=self.event)

        else:
            # create version from existing, and
            # put and link last event to new version
            if self.version is None:
                self.version = RunVersion(
                    parent_version=parent_run.version)
                self.version.add_level(self.id)

            if put_event:
                self.buffer.put_event(
                    nfa_name=self.nfa.name,
                    run_id=parent_run.id,
                    version=parent_run.version.get_version_as_str(),
                    state_label=self.current_state.label,
                    event=self.event,
                    new_run_id=self.id,
                    new_version=self.version.get_version_as_str())

        # immediately final if start state is final state
        if self.nfa.start_is_final:
            self.set_final(history=None, notify=False)

    @staticmethod
    def _generate_id(nfa_name: str, start_event_id: str) -> str:
        """
        Generates a run ID.

        :param nfa_name: The run NFA name.
        :type nfa_name: str

        :param start_event_id: The ID of the first event in the run,
        :type start_event_id: str

        :return: A run ID.
        """

        return "{}-{}".format(nfa_name, start_event_id)

    def process(self, event: BoboEvent, recent: List[BoboEvent]) -> None:
        """
        Process an event.

        :param event: The event to process.
        :type event: BoboEvent

        :param recent: Recently accepted complex events of the corresponding
                        automaton.
        :type recent: List[BoboEvent]

        :raises RuntimeError: Run has already halted.
        """
        with self._lock:
            if self._halted:
                raise RuntimeError("Run {} has already halted."
                                   .format(self.id))

            # get the history of the current run
            history = self.buffer.get_all_events(
                self.nfa.name,
                self.id,
                self.version)

            if self._any_preconditions_failed(event, history, recent) or \
                    self._any_haltconditions_passed(event, history, recent):
                self.set_halt()
            else:
                self._handle_state(self.current_state, event, history, recent)

    def last_process_cloned(self) -> bool:
        """
        :return: True if the last time the run processed an event, a clone
                 occurred, False otherwise.
        """

        return self._last_process_cloned

    def set_cloned(self) -> None:
        """Set the run as having been cloned."""

        self._last_process_cloned = True

    def is_halted(self) -> bool:
        """
        :return: True if the run has halted, False otherwise.
        """

        with self._lock:
            return self._halted

    def is_final(self) -> bool:
        """
        :return: True if run has reached its final state, False otherwise.
        """

        with self._lock:
            return self._final

    def set_halt(self, notify: bool = True) -> None:
        """
        Halt the run.

        :param notify: Whether to notify run subscribers of the halting,
                       defaults to True.
        :type notify: bool, optional
        """

        with self._lock:
            if not self._halted:
                self._halted = True

                if notify:
                    self._notify_halt()

    def set_final(self,
                  history: BoboHistory = None,
                  notify: bool = True) -> None:
        """
        Put run into final state.

        :param history: The history of the run.
        :type history: BoboHistory

        :param notify: Whether to notify subscribers of the transition to
                       its final state, defaults to True.
        :type notify: bool, optional
        """

        with self._lock:
            if not self._final:
                if notify:
                    self._notify_final(
                        history if history is not None else
                        self.buffer.get_all_events(
                            nfa_name=self.nfa.name,
                            run_id=self.id,
                            version=self.version))

                # do not notify halt if final
                self.set_halt(notify=False)

                self._final = True

    def subscribe(self, subscriber: IRunSubscriber) -> None:
        """
        :param subscriber: Subscribes to the run.
        :type subscriber: IRunSubscriber
        """

        with self._lock:
            if subscriber not in self._subs:
                self._subs.append(subscriber)

    def unsubscribe(self, unsubscriber: IRunSubscriber) -> None:
        """
        :param unsubscriber: Unsubscribes from the run.
        :type unsubscriber: IRunSubscriber

        :raises RuntimeError: Run has already halted.
        """

        with self._lock:
            if unsubscriber in self._subs:
                self._subs.remove(unsubscriber)

    def to_dict(self) -> dict:
        """
        :return: A dict representation of the object.
        """

        with self._lock:
            return {
                self.NFA_NAME: self.nfa.name,
                self.EVENT: self.event.to_dict(),
                self.START_TIME: self.start_time,
                self.START_STATE_NAME: self.start_state.name,
                self.CURRENT_STATE_NAME: self.current_state.name,
                self.RUN_ID: self.id,
                self.VERSION: copy(self.version._levels),
                self.HALTED: self._halted,
                self.LAST_PROCESS_CLONED: self._last_process_cloned
            }

    def _handle_state(self,
                      state: BoboState,
                      event: BoboEvent,
                      history: BoboHistory,
                      recent: List[BoboEvent]) -> None:
        transition = self.nfa.transitions.get(state.name)

        if transition is None:
            raise RuntimeError("No transition found for state {}."
                               .format(state.name))

        for trans_state_name in transition.state_names:
            # get transition state from NFA
            trans_state = self.nfa.states[trans_state_name]

            # state successfully fulfilled
            if trans_state.process(event, history, recent):
                # negated i.e. should NOT have occurred, so halt
                if trans_state.is_negated:
                    self.set_halt()
                    break

                if not transition.is_deterministic:
                    # not a self loop: clone run
                    if trans_state_name != state.name:
                        self._notify_clone(trans_state, event)
                        self._last_process_cloned = True
                    else:
                        # increment current run
                        history = self._proceed(
                            event=event,
                            original_state=state,
                            trans_state=trans_state,
                            increment=self._last_process_cloned)
                        self._last_process_cloned = False
                else:
                    # deterministic: proceed as normal
                    history = self._proceed(event, state, trans_state)
            else:
                # if optional, or if state is negated: move to the next state
                if transition.is_deterministic and \
                        (trans_state.is_optional or trans_state.is_negated):
                    self._handle_state(trans_state, event, history, recent)

                # halt if requires strict contiguity
                elif transition.is_strict:
                    self.set_halt()
                    break

    def _proceed(self,
                 event: BoboEvent,
                 original_state: BoboState,
                 trans_state: BoboState,
                 increment: bool = False,
                 notify: bool = True) -> BoboHistory:

        if original_state.name not in self.nfa.states.keys():
            raise RuntimeError(
                "Original state {} not in NFA {}.".format(
                    original_state.name, self.nfa.name))

        if trans_state.name not in self.nfa.states.keys():
            raise RuntimeError(
                "Transition state {} not in NFA {}.".format(
                    original_state.name, self.nfa.name))

        if increment:
            new_increment = BoboRun._generate_id(
                nfa_name=self.nfa.name,
                start_event_id=event.event_id)
            new_version = self.version.list_to_version_str(
                self.version.get_version_as_list()[:-1] + [new_increment])
        else:
            new_increment = None
            new_version = None

        # add new event to buffer
        self.buffer.put_event(
            nfa_name=self.nfa.name,
            run_id=self.id,
            version=self.version.get_version_as_str(),
            state_label=trans_state.label,
            event=event,
            new_version=new_version)

        # (maybe) apply increment
        if new_increment is not None:
            self.version.increment_level(new_increment)

        # get run history
        new_history = self.buffer.get_all_events(
            self.nfa.name,
            self.id,
            self.version)

        # halt if final, else transition
        if self.nfa.final_state.name == trans_state.name:
            self.set_final(new_history, notify=notify)

        elif notify:
            self._notify_transition(
                original_state.name,
                trans_state.name,
                event)

        # update run
        self.current_state = trans_state
        self.event = event

        return new_history

    def _any_preconditions_failed(self,
                                  event: BoboEvent,
                                  history: BoboHistory,
                                  recent: List[BoboEvent]) -> bool:
        """If any preconditions are False, return True."""

        return any(not p.evaluate(event, history, recent)
                   for p in self.nfa.preconditions)

    def _any_haltconditions_passed(self,
                                   event: BoboEvent,
                                   history: BoboHistory,
                                   recent: List[BoboEvent]) -> bool:
        """If any haltconditions are True, return True."""

        return any(p.evaluate(event, history, recent)
                   for p in self.nfa.haltconditions)

    def _notify_transition(self,
                           state_name_from: str,
                           state_name_to: str,
                           event: BoboEvent) -> None:
        for subscriber in self._subs:
            subscriber.on_run_transition(
                run_id=self.id,
                state_name_from=state_name_from,
                state_name_to=state_name_to,
                event=event,
                notify=True)

    def _notify_clone(self,
                      state: BoboState,
                      next_event: BoboEvent) -> None:
        for subscriber in self._subs:
            subscriber.on_run_clone(
                state_name=state.name,
                event=next_event,
                parent_run_id=self.id,
                force_parent=False,
                notify=True)

    def _notify_final(self,
                      history: BoboHistory) -> None:
        for subscriber in self._subs:
            subscriber.on_run_final(
                run_id=self.id,
                history=history,
                notify=True)

    def _notify_halt(self) -> None:
        for subscriber in self._subs:
            subscriber.on_run_halt(
                run_id=self.id,
                notify=True)