# Copyright (C) 2018 Lars Wirzenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import ick2 BUILD_TRIGGERED = 'triggered' BUILD_BUILDING = 'building' BUILD_NOTIFYING = 'notifying' BUILD_DONE = 'done' BUILD_FAILED = 'failed' class StateMachine: def __init__(self, get_state, set_state): self.transitions = {} self.get_state = get_state self.set_state = set_state def add_transition(self, state, event_class, handler): if state not in self.transitions: self.transitions[state] = {} assert event_class not in self.transitions[state] self.transitions[state][event_class] = handler def handle_event(self, event): state = self.get_state() assert state in self.transitions assert event.__class__ in self.transitions[state] func = self.transitions[state][event.__class__] new_state, resp = func(event) self.set_state(new_state) return resp class BuildStateMachine: def __init__(self, build): self.build = build self.sm = self.init_sm() def init_sm(self): transitions = { BUILD_TRIGGERED: { BuildStartsEvent: self.set_state_to_building, }, BUILD_BUILDING: { NeedWorkEvent: self.pick_action, PartialActionOutputEvent: self.set_state_to_building, ActionFinishedEvent: self.mark_action_done, ActionFailedEvent: self.mark_build_failed, }, BUILD_NOTIFYING: { NeedWorkEvent: self.pick_notification_action, PartialActionOutputEvent: self.set_state_to_notifying, ActionFinishedEvent: self.mark_notification_done, ActionFailedEvent: self.mark_build_failed, }, } sm = StateMachine(self.get_state, self.set_state) for state in transitions: for event_class in transitions[state]: func = transitions[state][event_class] sm.add_transition(state, event_class, func) return sm def get_state(self): return self.build.resource['status'] def set_state(self, state): self.build.resource['status'] = state def handle_event(self, event): old_state = self.get_state() resp = self.sm.handle_event(event) new_state = self.get_state() return resp def set_state_to_building(self, event): return BUILD_BUILDING, None def set_state_to_notifying(self, event): # pragma: no cover return BUILD_NOTIFYING, None def pick_action(self, event): graph = self.build.get_graph() action_ids = graph.find_actions(ick2.ACTION_READY) if not action_ids: # pragma: no cover self.build.resource['exit_code'] = 0 return BUILD_DONE, None action_id = action_ids[0] graph.set_action_status(action_id, ick2.ACTION_BUILDING) action = graph.get_action(action_id) return BUILD_BUILDING, (action_id, action) def pick_notification_action(self, event): graph = self.build.get_graph() action_ids = graph.find_actions(ick2.ACTION_READY) if not action_ids: # pragma: no cover self.build.resource['exit_code'] = 0 return BUILD_DONE, None action_id = action_ids[0] graph.set_action_status(action_id, ick2.ACTION_BUILDING) action = graph.get_action(action_id) return BUILD_NOTIFYING, (action_id, action) def mark_action_done(self, event): self.build.resource['exit_code'] = event.exit_code graph = self.build.get_graph() graph.set_action_status(event.action_id, ick2.ACTION_DONE) graph.unblock() if graph.has_more_to_do(): return BUILD_BUILDING, None self.add_notification_action() return BUILD_NOTIFYING, None def add_notification_action(self): action = { 'action': 'notify', } graph = self.build.get_graph() graph.append_action(action, ick2.ACTION_READY, depends=[]) def mark_notification_done(self, event): if event.exit_code not in (0, None): # pragma: no cover self.build.resource['exit_code'] = event.exit_code graph = self.build.get_graph() graph.set_action_status(event.action_id, ick2.ACTION_DONE) graph.unblock() if graph.has_more_to_do(): # pragma: no cover return BUILD_NOTIFYING, None if self.build.resource.get('exit_code') in (0, None): self.build.resource['exit_code'] = 0 return BUILD_DONE, None return BUILD_FAILED, None def mark_build_failed(self, event): graph = self.build.get_graph() graph.set_action_status(event.action_id, ick2.BUILD_FAILED) self.build.resource['exit_code'] = event.exit_code self.add_notification_action() return BUILD_NOTIFYING, None # Thing should be something we can create a BuildEvent from. def create_build_event(thing): if thing == BuildStartsEvent: return BuildStartsEvent() if thing == NeedWorkEvent: return NeedWorkEvent() if isinstance(thing, dict): exit_code = thing.get('exit_code') action_id = thing.get('action_id') if exit_code is None: return PartialActionOutputEvent() if exit_code == 0: return ActionFinishedEvent(action_id) return ActionFailedEvent(action_id, exit_code) class BuildEvent: # pragma: no cover event_type = 'BuildEvent' def __str__(self): return self.event_type class BuildStartsEvent(BuildEvent): event_type = 'build-starts' class NeedWorkEvent(BuildEvent): event_type = 'need-work' class PartialActionOutputEvent(BuildEvent): event_type = 'partial-output' class ActionFinishedEvent(BuildEvent): event_type = 'action-finished' def __init__(self, action_id): self.action_id = action_id self.exit_code = 0 class ActionFailedEvent(BuildEvent): event_type = 'action-failed' def __init__(self, action_id, exit_code): self.action_id = action_id self.exit_code = exit_code class UnexpectedEvent(Exception): # pragma: no cover def __init__(self, event, state): super().__init__('Did not expect %s in %s' % (event, state))