xref: /aosp_15_r20/external/autotest/client/site_tests/cellular_SuspendResume/cellular_SuspendResume.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10from functools import reduce
11
12import dbus
13import logging
14from random import choice, randint
15import time
16
17from six.moves import range
18
19from autotest_lib.client.bin import test, utils
20from autotest_lib.client.common_lib import error
21from autotest_lib.client.common_lib.cros import chrome
22from autotest_lib.client.cros import rtc
23from autotest_lib.client.cros.power import power_suspend
24
25# Special import to define the location of the flimflam library.
26from autotest_lib.client.cros import flimflam_test_path
27
28import flimflam
29
30
31SHILL_LOG_SCOPES = 'cellular+dbus+device+dhcp+manager+modem+portal+service'
32
33class cellular_SuspendResume(test.test):
34    version = 1
35    TIMEOUT = 60
36
37    device_okerrors = [
38        # Setting of device power can sometimes result with InProgress error
39        # if it is in the process of already doing so.
40        'org.chromium.flimflam.Error.InProgress',
41    ]
42
43    service_okerrors = [
44        'org.chromium.flimflam.Error.InProgress',
45        'org.chromium.flimflam.Error.AlreadyConnected',
46    ]
47
48    scenarios = {
49        'all': [
50            'scenario_suspend_mobile_enabled',
51            'scenario_suspend_mobile_disabled',
52            'scenario_suspend_mobile_disabled_twice',
53            'scenario_autoconnect',
54        ],
55        'stress': [
56            'scenario_suspend_mobile_random',
57        ],
58    }
59
60    modem_status_checks = [
61        lambda s: ('org/chromium/ModemManager' in s) or
62                  ('org/freedesktop/ModemManager' in s) or
63                  ('org/freedesktop/ModemManager1' in s),
64        lambda s: ('meid' in s) or ('EquipmentIdentifier' in s),
65        lambda s: 'Manufacturer' in s,
66        lambda s: 'Device' in s
67    ]
68
69    def filterexns(self, function, exn_list):
70        try:
71            function()
72        except dbus.exceptions.DBusException as e:
73            if e._dbus_error_name not in exn_list:
74                raise e
75
76    # This function returns True when mobile service is available.  Otherwise,
77    # if the timeout period has been hit, it returns false.
78    def mobile_service_available(self, timeout=60):
79        service = self.flim.FindCellularService(timeout)
80        if service:
81            logging.info('Mobile service is available.')
82            return service
83        logging.info('Mobile service is not available.')
84        return None
85
86    def get_powered(self, device):
87        properties = device.GetProperties()
88        logging.debug(properties)
89        logging.info('Power state of mobile device is %s.',
90                     ['off', 'on'][properties['Powered']])
91        return properties['Powered']
92
93    def _check_powered(self, device, check_enabled):
94        properties = device.GetProperties()
95        power_state = (properties['Powered'] == 1)
96        return power_state if check_enabled else not power_state
97
98    def check_powered(self, device, check_enabled):
99        logging.info('Polling to check device state is %s.',
100                     'enabled' if check_enabled else 'disabled')
101        utils.poll_for_condition(
102            lambda: self._check_powered(device, check_enabled),
103            exception=error.TestFail(
104                'Failed to verify the device is in power state %s.',
105                'enabled' if check_enabled else 'disabled'),
106            timeout=self.TIMEOUT)
107        logging.info('Verified device power state.')
108
109    def enable_device(self, device, enable):
110        lambda_func = lambda: device.Enable() if enable else device.Disable()
111        self.filterexns(lambda_func,
112                        cellular_SuspendResume.device_okerrors)
113        # Sometimes if we disable the modem then immediately enable the modem
114        # we hit a condition where the modem seems to ignore the enable command
115        # and keep the modem disabled.  This is to prevent that from happening.
116        time.sleep(4)
117        return self.get_powered(device) == enable
118
119    def suspend_resume(self, duration=10):
120        suspender = power_suspend.Suspender(self.resultsdir, throw=True)
121        suspender.suspend(duration)
122        logging.info('Machine resumed')
123
124        # Race condition hack alert: Before we added this sleep, this
125        # test was very sensitive to the relative timing of the test
126        # and modem resumption.  There is a window where flimflam has
127        # not yet learned that the old modem has gone away (it doesn't
128        # find this out until seconds after we resume) and the test is
129        # running.  If the test finds and attempts to use the old
130        # modem, those operations will fail.  There's no good
131        # hardware-independent way to see the modem go away and come
132        # back, so instead we sleep
133        time.sleep(4)
134
135    # __get_mobile_device is a hack wrapper around the flim.FindCellularDevice
136    # that verifies that GetProperties can be called before proceeding.
137    # There appears to be an issue after suspend/resume where GetProperties
138    # returns with UnknownMethod called until some time later.
139    def __get_mobile_device(self, timeout=TIMEOUT):
140        device = None
141        properties = None
142        start_time = time.time()
143        timeout = start_time + timeout
144        while properties is None and time.time() < timeout:
145            try:
146                device = self.flim.FindCellularDevice(timeout)
147                properties = device.GetProperties()
148            except dbus.exceptions.DBusException:
149                logging.debug('Mobile device not ready yet')
150                device = None
151                properties = None
152
153            time.sleep(1)
154
155        if not device:
156            # If device is not found, spit the output of lsusb for debugging.
157            lsusb_output = utils.system_output('lsusb', timeout=self.TIMEOUT)
158            logging.debug('Mobile device not found. lsusb output:')
159            logging.debug(lsusb_output)
160            raise error.TestError('Mobile device not found.')
161        return device
162
163    # The suspend_mobile_enabled test suspends, then resumes the machine while
164    # mobile is enabled.
165    def scenario_suspend_mobile_enabled(self, **kwargs):
166        device = self.__get_mobile_device()
167        self.enable_device(device, True)
168        if not self.mobile_service_available():
169            raise error.TestError('Unable to find mobile service.')
170        self.suspend_resume(20)
171
172    # The suspend_mobile_disabled test suspends, then resumes the machine
173    # while mobile is disabled.
174    def scenario_suspend_mobile_disabled(self, **kwargs):
175        device = self.__get_mobile_device()
176        self.enable_device(device, False)
177        self.suspend_resume(20)
178
179        # This verifies that the device is in the same state before and after
180        # the device is suspended/resumed.
181        device = self.__get_mobile_device()
182        logging.info('Checking to see if device is in the same state as prior '
183                     'to suspend/resume')
184        self.check_powered(device, False)
185
186        # Turn on the device to make sure we can bring it back up.
187        self.enable_device(device, True)
188
189    # The suspend_mobile_disabled_twice subroutine is here because
190    # of bug 9405.  The test will suspend/resume the device twice
191    # while mobile is disabled.  We will then verify that mobile can be
192    # enabled thereafter.
193    def scenario_suspend_mobile_disabled_twice(self, **kwargs):
194        device = self.__get_mobile_device()
195        self.enable_device(device, False)
196
197        for _ in [0, 1]:
198            self.suspend_resume(20)
199
200            # This verifies that the device is in the same state before
201            # and after the device is suspended/resumed.
202            device = self.__get_mobile_device()
203            logging.info('Checking to see if device is in the same state as '
204                         'prior to suspend/resume')
205            self.check_powered(device, False)
206
207        # Turn on the device to make sure we can bring it back up.
208        self.enable_device(device, True)
209
210
211    # This test randomly enables or disables the modem.  This is mainly used
212    # for stress tests as it does not check the power state of the modem before
213    # and after suspend/resume.
214    def scenario_suspend_mobile_random(self, stress_iterations=10, **kwargs):
215        logging.debug('Running suspend_mobile_random %d times' %
216                      stress_iterations)
217        device = self.__get_mobile_device()
218        self.enable_device(device, choice([True, False]))
219
220        # Suspend the device for a random duration, wake it,
221        # wait for the service to appear, then wait for
222        # some random duration before suspending again.
223        for i in range(stress_iterations):
224            logging.debug('Running iteration %d' % (i+1))
225            self.suspend_resume(randint(10, 40))
226            device = self.__get_mobile_device()
227            self.enable_device(device, True)
228            if not self.flim.FindCellularService(self.TIMEOUT*2):
229                raise error.TestError('Unable to find mobile service')
230            time.sleep(randint(1, 30))
231
232
233    # This verifies that autoconnect works.
234    def scenario_autoconnect(self, **kwargs):
235        device = self.__get_mobile_device()
236        self.enable_device(device, True)
237        service = self.flim.FindCellularService(self.TIMEOUT)
238        if not service:
239            raise error.TestError('Unable to find mobile service')
240
241        props = service.GetProperties()
242        if props['AutoConnect']:
243            expected_states = ['ready', 'online', 'portal']
244        else:
245            expected_states = ['idle']
246
247        for _ in range(5):
248            # Must wait at least 20 seconds to ensure that the suspend occurs
249            self.suspend_resume(20)
250
251            # wait for the device to come back
252            device = self.__get_mobile_device()
253
254            # verify the service state is correct
255            service = self.flim.FindCellularService(self.TIMEOUT)
256            if not service:
257                raise error.TestFail('Cannot find mobile service')
258
259            state, _ = self.flim.WaitForServiceState(service,
260                                                     expected_states,
261                                                     self.TIMEOUT)
262            if not state in expected_states:
263                raise error.TestFail('Mobile state %s not in %s as expected'
264                                     % (state, ', '.join(expected_states)))
265
266
267    # Returns 1 if modem_status returned output within duration.
268    # otherwise, returns 0
269    def _get_modem_status(self, duration=TIMEOUT):
270        time_end = time.time() + duration
271        while time.time() < time_end:
272            status = utils.system_output('modem status', timeout=self.TIMEOUT)
273            if reduce(lambda x, y: x & y(status),
274                      cellular_SuspendResume.modem_status_checks,
275                      True):
276                break
277        else:
278            return 0
279        return 1
280
281    # This is the wrapper around the running of each scenario with
282    # initialization steps and final checks.
283    def run_scenario(self, function_name, **kwargs):
284        device = self.__get_mobile_device()
285
286        # Initialize all tests with the power off.
287        self.enable_device(device, False)
288
289        function = getattr(self, function_name)
290        logging.info('Running %s' % function_name)
291        function(**kwargs)
292
293        # By the end of each test, the mobile device should be up.
294        # Here we verify that the power state of the device is up, and
295        # that the mobile service can be found.
296        device = self.__get_mobile_device()
297        logging.info('Checking that modem is powered on after scenario %s.',
298                     function_name)
299        self.check_powered(device, True)
300
301        logging.info('Scenario complete: %s.' % function_name)
302
303        if not self._get_modem_status():
304            raise error.TestFail('Failed to get modem_status after %s.'
305                              % function_name)
306        service = self.mobile_service_available()
307        if not service:
308            raise error.TestFail('Could not find mobile service at the end '
309                                 'of test %s.' % function_name)
310
311    def init_flimflam(self):
312        # Initialize flimflam and device type specific functions.
313        self.flim = flimflam.FlimFlam(dbus.SystemBus())
314        self.flim.SetDebugTags(SHILL_LOG_SCOPES)
315
316        self.flim.FindCellularService = self.flim.FindCellularService
317        self.flim.FindCellularDevice = self.flim.FindCellularDevice
318
319    def run_once(self, scenario_group='all', autoconnect=False, **kwargs):
320
321        with chrome.Chrome():
322            # Replace the test type with the list of tests
323            if (scenario_group not in
324                    list(cellular_SuspendResume.scenarios.keys())):
325                scenario_group = 'all'
326            logging.info('Running scenario group: %s' % scenario_group)
327            scenarios = cellular_SuspendResume.scenarios[scenario_group]
328
329            self.init_flimflam()
330
331            device = self.__get_mobile_device()
332            if not device:
333                raise error.TestFail('Cannot find mobile device.')
334            self.enable_device(device, True)
335
336            service = self.flim.FindCellularService(self.TIMEOUT)
337            if not service:
338                raise error.TestFail('Cannot find mobile service.')
339
340            service.SetProperty('AutoConnect', dbus.Boolean(autoconnect))
341
342            logging.info('Running scenarios with autoconnect %s.' % autoconnect)
343
344            for t in scenarios:
345                self.run_scenario(t, **kwargs)
346