# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import servo_afe_board_map from autotest_lib.server import test from autotest_lib.server.cros.servo import servo def _successful(result_value): return result_value and not isinstance(result_value, Exception) class _DiagnosticTest(object): """Data needed to handle one diagnostic test on a Servo host. The class encapsulates two basic elements: 1. A pre-requisite test that must have passed. The pre-requisite is recorded as a key in the results dictionary. 2. A function that performs the actual diagnostic test. All tests have the implicit pre-requisite that the servo host can be reached on the network via ping. Pre-requisites are meant to capture relationships of the form "if test X cant't pass, test Y will always fail". Typically, that means that test X tests a capability used by test Y. This implementation is a bit naive: It assumes only a single pre-requisite, and it assumes the only outcome is a simple pass/fail. The design also doesn't account for relationships of the form "if test X fails, run test Y to try and distinguish possible causes". """ def __init__(self, prerequisite, get_result): self._prerequisite = prerequisite self._get_result = get_result def can_run(self, results): """Return whether this test's pre-requisite is satisfied. @param results The results dictionary with the status of this test's pre-requisite. """ if self._prerequisite is None: return True return _successful(results[self._prerequisite]) def run_diagnostic(self, servo_host, servod): """Run the diagnostic test, and return the result. The test receives ServoHost and Servo objects to be tested; typically a single test uses one or the other, but not both. @param servo_host A ServoHost object to be the target of the test. @param servod A Servo object to be the target of the test. @return If the test returns normally, return its result. If the test raises an exception, return the exception. """ try: return self._get_result(servo_host, servod) except Exception as e: return e def _ssh_test(servo_host, servod): """Test whether the servo host answers to ssh. This test serves as a basic pre-requisite for tests that use ssh to test other conditions. Pre-requisite: There are no pre-requisites for this test aside from the implicit pre-requisite that the host answer to ping. @param servo_host The ServoHost object to talk to via ssh. @param servod Ignored. """ return servo_host.is_up() def _servod_connect(servo_host, servod): """Test whether connection to servod succeeds. This tests the connection to the target servod with a simple method call. As a side-effect, all hardware signals are initialized to default values. This function always returns success. The test can only fail if the underlying call to servo raises an exception. Pre-requisite: There are no pre-requisites for this test aside from the implicit pre-requisite that the host answer to ping. @return `True` """ # TODO(jrbarnette) We need to protect this call so that it # will time out if servod doesn't respond. servod.initialize_dut() return True def _pwr_button_test(servo_host, servod): """Test whether the 'pwr_button' signal is correct. This tests whether the state of the 'pwr_button' signal is 'release'. When the servo flex cable is not attached, the signal will be stuck at 'press'. Pre-requisite: This test depends on successful initialization of servod. Rationale: The initialization step sets 'pwr_button' to 'release', which is required to justify the expectations of this test. Also, if initialization fails, we can reasonably expect that all communication with servod will fail. @param servo_host Ignored. @param servod The Servo object to be tested. """ return servod.get('pwr_button') == 'release' def _lid_test(servo_host, servod): """Test whether the 'lid_open' signal is correct. This tests whether the state of the 'lid_open' signal has a correct value. There is a manual switch on the servo board; if that switch is set wrong, the signal will be stuck at 'no'. Working units may return a setting of 'yes' (meaning the lid is open) or 'not_applicable' (meaning the device has no lid). Pre-requisite: This test depends on the 'pwr_button' test. Rationale: If the 'pwr_button' test fails, the flex cable may be disconnected, which means any servo operation to read a hardware signal will fail. @param servo_host Ignored. @param servod The Servo object to be tested. """ return servod.get('lid_open') != 'no' def _command_test(servo_host, command): """Utility to return the output of a command on a servo host. The command is expected to produce at most one line of output. A trailing newline, if any, is stripped. @return Output from the command with the trailing newline removed. """ return servo_host.run(command).stdout.strip('\n') def _brillo_test(servo_host, servod): """Get the version of Brillo running on the servo host. Reads the setting of CHROMEOS_RELEASE_VERSION from /etc/lsb-release on the servo host. An empty string will returned if there is no such setting. Pre-requisite: This test depends on the ssh test. @param servo_host The ServoHost object to be queried. @param servod Ignored. @return Returns a Brillo version number or an empty string. """ command = ('sed "s/CHROMEOS_RELEASE_VERSION=//p ; d" ' '/etc/lsb-release') return _command_test(servo_host, command) def _board_test(servo_host, servod): """Get the board for which the servo is configured. Reads the setting of BOARD from /var/lib/servod/config. An empty string is returned if the board is unconfigured. Pre-requisite: This test depends on the brillo version test. Rationale: The /var/lib/servod/config file is used by the servod upstart job, which is specific to Brillo servo builds. This test has no meaning if the target servo host isn't running Brillo. @param servo_host The ServoHost object to be queried. @param servod Ignored. @return The confgured board or an empty string. """ command = ('CONFIG=/var/lib/servod/config\n' '[ -f $CONFIG ] && . $CONFIG && echo $BOARD') return _command_test(servo_host, command) def _servod_test(servo_host, servod): """Get the status of the servod upstart job. Ask upstart for the status of the 'servod' job. Return whether the job is reported running. Pre-requisite: This test depends on the brillo version test. Rationale: The servod upstart job is specific to Brillo servo builds. This test has no meaning if the target servo host isn't running Brillo. @param servo_host The ServoHost object to be queried. @param servod Ignored. @return `True` if the job is running, or `False` otherwise. """ command = 'status servod | sed "s/,.*//"' return _command_test(servo_host, command) == 'servod start/running' _DIAGNOSTICS_LIST = [ ('ssh_responds', _DiagnosticTest(None, _ssh_test)), ('servod_connect', _DiagnosticTest(None, _servod_connect)), ('pwr_button', _DiagnosticTest('servod_connect', _pwr_button_test)), ('lid_open', _DiagnosticTest('pwr_button', _lid_test)), ('brillo_version', _DiagnosticTest('ssh_responds', _brillo_test)), ('board', _DiagnosticTest('brillo_version', _board_test)), ('servod', _DiagnosticTest('brillo_version', _servod_test)), ] class infra_ServoDiagnosis(test.test): """Test a servo and diagnose common failures.""" version = 1 def _run_results(self, servo_host, servod): results = {} for key, tester in _DIAGNOSTICS_LIST: if tester.can_run(results): results[key] = tester.run_diagnostic(servo_host, servod) logging.info('Test %s result %s', key, results[key]) else: results[key] = None logging.info('Skipping %s', key) return results def run_once(self, host): """Test and diagnose the servo for the given host. @param host Host object for a DUT with Servo. """ # TODO(jrbarnette): Need to handle ping diagnoses: # + Specifically report if servo host isn't a lab host. # + Specifically report if servo host is in lab but # doesn't respond to ping. servo_host = host._servo_host servod = host.servo if servod is None: servod = servo.Servo(servo_host) results = self._run_results(servo_host, servod) if not _successful(results['ssh_responds']): raise error.TestFail('ssh connection to %s failed' % servo_host.hostname) if not _successful(results['brillo_version']): raise error.TestFail('Servo host %s is not running Brillo' % servo_host.hostname) # Make sure servo board matches DUT label board = host._get_board_from_afe() board = servo_afe_board_map.map_afe_board_to_servo_board(board) if (board and results['board'] is not None and board != results['board']): logging.info('AFE says board should be %s', board) if results['servod']: servo_host.run('stop servod', ignore_status=True) servo_host.run('start servod BOARD=%s' % board) results = self._run_results(servo_host, servod) # TODO(jrbarnette): The brillo update check currently # lives in ServoHost; it needs to move here. # Repair actions: # if servod is dead or running but not working # reboot and re-run results if (not _successful(results['servod']) or not _successful(results['servod_connect'])): # TODO(jrbarnette): For now, allow reboot failures to # raise their exceptions up the stack. This event # shouldn't happen, so smarter handling should wait # until we have a use case to guide the requirements. servo_host.reboot() results = self._run_results(servo_host, servod) if not _successful(results['servod']): # write result value to log raise error.TestFail('servod failed to start on %s' % servo_host.hostname) if not _successful(results['servod_connect']): raise error.TestFail('Servo failure on %s' % servo_host.hostname) if not _successful(results['pwr_button']): raise error.TestFail('Stuck power button on %s' % servo_host.hostname) if not _successful(results['lid_open']): raise error.TestFail('Lid stuck closed on %s' % servo_host.hostname)