summaryrefslogtreecommitdiff
path: root/ick2/buildsm.py
diff options
context:
space:
mode:
Diffstat (limited to 'ick2/buildsm.py')
-rw-r--r--ick2/buildsm.py223
1 files changed, 223 insertions, 0 deletions
diff --git a/ick2/buildsm.py b/ick2/buildsm.py
new file mode 100644
index 0000000..2e6d079
--- /dev/null
+++ b/ick2/buildsm.py
@@ -0,0 +1,223 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+
+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):
+ 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):
+ 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') is 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
+
+
+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))