blob: 27d209818cd423cb4b897ba9c07fb7f2652ef6a9 [file] [log] [blame]
# 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)