• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import dbus
6import dbus.service
7# AU tests use ToT client code, but ToT -3 client version.
8try:
9    from gi.repository import GObject
10except ImportError:
11    import gobject as GObject
12import logging
13
14from . import pm_errors
15from . import pm_constants
16from . import utils
17
18from autotest_lib.client.cros.cellular import mm1_constants
19
20class StateMachine(dbus.service.Object):
21    """
22    StateMachine is the abstract base class for the complex state machines
23    that are involved in the pseudo modem manager.
24
25    Every state transition is managed by a function that has been mapped to a
26    specific modem state. For example, the method that handles the case where
27    the modem is in the ENABLED state would look like:
28
29        def _HandleEnabledState(self):
30            # Do stuff.
31
32    The correct method will be dynamically located and executed by the step
33    function according to the dictionary returned by the subclass'
34    implementation of StateMachine._GetModemStateFunctionMap.
35
36    Using the StateMachine in |interactive| mode:
37    In interactive mode, the state machine object exposes a dbus object under
38    the object path |pm_constants.TESTING_PATH|/|self._GetIsmObjectName()|,
39    where |self._GetIsmObjectName()| returns the dbus object name to be used.
40
41    In this mode, the state machine waits for a dbus method call
42    |pm_constants.I_TESTING_ISM|.|Advance| when a state transition is possible
43    before actually executing the transition.
44
45    """
46    def __init__(self, modem):
47        super(StateMachine, self).__init__(None, None)
48        self._modem = modem
49        self._started = False
50        self._done = False
51        self._interactive = False
52        self._trans_func_map = self._GetModemStateFunctionMap()
53
54
55    def __exit__(self):
56        self.remove_from_connection()
57
58
59    @property
60    def cancelled(self):
61        """
62        @returns: True, if the state machine has been cancelled or has
63                transitioned to a terminal state. False, otherwise.
64
65        """
66        return self._done
67
68
69    def Cancel(self):
70        """
71        Tells the state machine to stop transitioning to further states.
72
73        """
74        self._done = True
75
76
77    def EnterInteractiveMode(self, bus):
78        """
79        Run this machine in interactive mode.
80
81        This function must be called before |Start|. In this mode, the machine
82        waits for an |Advance| call before each step.
83
84        @param bus: The bus on which the testing interface must be exported.
85
86        """
87        if not bus:
88            self.warning('Cannot enter interactive mode without a |bus|.')
89            return
90
91        self._interactive = True
92        self._ism_object_path = '/'.join([pm_constants.TESTING_PATH,
93                                          self._GetIsmObjectName()])
94        self.add_to_connection(bus, self._ism_object_path)
95        self._interactive = True
96        self._waiting_for_advance = False
97        logging.info('Running state machine in interactive mode')
98        logging.info('Exported test object at %s', self._ism_object_path)
99
100
101    def Start(self):
102        """ Start the state machine. """
103        self.Step()
104
105
106    @utils.log_dbus_method()
107    @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b')
108    def Advance(self):
109        """
110        Advance a step on a state machine running in interactive mode.
111
112        @returns: True if the state machine was advanced. False otherwise.
113        @raises: TestError if called on a non-interactive state machine.
114
115        """
116        if not self._interactive:
117            raise pm_errors.TestError(
118                    'Can not advance a non-interactive state machine')
119
120        if not self._waiting_for_advance:
121            logging.warning('%s received an unexpected advance request',
122                            self._GetIsmObjectName())
123            return False
124        logging.info('%s state machine advancing', self._GetIsmObjectName())
125        self._waiting_for_advance = False
126        if not self._next_transition(self):
127            self._done = True
128        self._ScheduleNextStep()
129        return True
130
131
132    @dbus.service.signal(pm_constants.I_TESTING_ISM)
133    def Waiting(self):
134        """
135        Signal sent out by an interactive machine when it is waiting for remote
136        dbus call  on the |Advance| function.
137
138        """
139        logging.info('%s state machine waiting', self._GetIsmObjectName())
140
141
142    @utils.log_dbus_method()
143    @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b')
144    def IsWaiting(self):
145        """
146        Determine whether the state machine is waiting for user action.
147
148        @returns: True if machine is waiting for |Advance| call.
149
150        """
151        return self._waiting_for_advance
152
153
154    def Step(self):
155        """
156        Executes the next corresponding state transition based on the modem
157        state.
158
159        """
160        logging.info('StateMachine: Step')
161        if self._done:
162            logging.info('StateMachine: Terminating.')
163            return
164
165        if not self._started:
166            if not self._ShouldStartStateMachine():
167                logging.info('StateMachine cannot start.')
168                return
169            self._started = True
170
171        state = self._GetCurrentState()
172        func = self._trans_func_map.get(state, self._GetDefaultHandler())
173        if not self._interactive:
174            if func and func(self):
175                self._ScheduleNextStep()
176            else:
177                self._done = True
178            return
179
180        assert not self._waiting_for_advance
181        if func:
182            self._next_transition = func
183            self._waiting_for_advance = True
184            self.Waiting()  # Wait for user to |Advance| the machine.
185        else:
186            self._done = True
187
188
189    def _ScheduleNextStep(self):
190        """
191        Schedules the next state transition to execute on the idle loop.
192        subclasses can override this method to implement custom logic, such as
193        delays.
194
195        """
196        GObject.idle_add(StateMachine.Step, self)
197
198
199    def _GetIsmObjectName(self):
200        """
201        The name of the dbus object exposed by this object with |I_TESTING_ISM|
202        interface.
203
204        By default, this is the name of the most concrete class of the object.
205
206        """
207        return self.__class__.__name__
208
209
210    def _GetDefaultHandler(self):
211        """
212        Returns the function to handle a modem state, for which the value
213        returned by StateMachine._GetModemStateFunctionMap is None. The
214        returned function's signature must match:
215
216            StateMachine -> Boolean
217
218        This function by default returns None. If no function exists to handle
219        a modem state, the default behavior is to terminate the state machine.
220
221        """
222        return None
223
224
225    def _GetModemStateFunctionMap(self):
226        """
227        Returns a mapping from modem states to corresponding transition
228        functions to execute. The returned function's signature must match:
229
230            StateMachine -> Boolean
231
232        The first argument to the function is a state machine, which will
233        typically be passed a value of |self|. The return value, if True,
234        indicates that the state machine should keep executing further state
235        transitions. A return value of False indicates that the state machine
236        will transition to a terminal state.
237
238        This method must be implemented by a subclass. Subclasses can further
239        override this method to provide custom functionality.
240
241        """
242        raise NotImplementedError()
243
244
245    def _ShouldStartStateMachine(self):
246        """
247        This method will be called when the state machine is in a starting
248        state. This method should return True, if the state machine can
249        successfully begin its state transitions, False if it should not
250        proceed. This method can also raise an exception in the failure case.
251
252        In the success case, this method should also execute any necessary
253        initialization steps.
254
255        This method must be implemented by a subclass. Subclasses can
256        further override this method to provide custom functionality.
257
258        """
259        raise NotImplementedError()
260
261
262    def _GetCurrentState(self):
263        """
264        Get the current state of the state machine.
265
266        This method is called to get the current state of the machine when
267        deciding what the next transition should be.
268        By default, the state machines are tied to the modem state, and this
269        function simply returns the modem state.
270
271        Subclasses can override this function to use custom states in the state
272        machine.
273
274        @returns: The modem state.
275
276        """
277        return self._modem.Get(mm1_constants.I_MODEM, 'State')
278