普通文本  |  337行  |  11.53 KB

# 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)