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())
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)
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())
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)
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())
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))
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)